Import Hermes from CVS
authorBen Klang <ben@alkaloid.net>
Wed, 26 May 2010 18:14:33 +0000 (14:14 -0400)
committerBen Klang <ben@alkaloid.net>
Wed, 2 Jun 2010 19:22:22 +0000 (15:22 -0400)
100 files changed:
hermes/LICENSE [new file with mode: 0644]
hermes/README [new file with mode: 0644]
hermes/admin.php [new file with mode: 0644]
hermes/config/.cvsignore [new file with mode: 0644]
hermes/config/.htaccess [new file with mode: 0644]
hermes/config/conf.xml [new file with mode: 0644]
hermes/config/prefs.php.dist [new file with mode: 0644]
hermes/deliverables.php [new file with mode: 0644]
hermes/docs/CHANGES [new file with mode: 0644]
hermes/docs/CREDITS [new file with mode: 0644]
hermes/docs/INSTALL [new file with mode: 0644]
hermes/docs/RELEASE_NOTES [new file with mode: 0644]
hermes/docs/TODO [new file with mode: 0644]
hermes/entry.php [new file with mode: 0644]
hermes/index.php [new file with mode: 0644]
hermes/invoicing.php [new file with mode: 0644]
hermes/lib/Admin.php [new file with mode: 0644]
hermes/lib/Block/tree_menu.php [new file with mode: 0644]
hermes/lib/Block/tree_stopwatch.php [new file with mode: 0644]
hermes/lib/Data/hermes_csv.php [new file with mode: 0644]
hermes/lib/Data/hermes_tsv.php [new file with mode: 0644]
hermes/lib/Data/hermes_xls.php [new file with mode: 0644]
hermes/lib/Data/iif.php [new file with mode: 0644]
hermes/lib/Data/qbxml.php [new file with mode: 0644]
hermes/lib/Driver.php [new file with mode: 0644]
hermes/lib/Driver/sql.php [new file with mode: 0644]
hermes/lib/Forms/Deliverable.php [new file with mode: 0644]
hermes/lib/Forms/Export.php [new file with mode: 0644]
hermes/lib/Forms/Search.php [new file with mode: 0644]
hermes/lib/Forms/Time.php [new file with mode: 0644]
hermes/lib/Hermes.php [new file with mode: 0644]
hermes/lib/Table.php [new file with mode: 0644]
hermes/lib/api.php [new file with mode: 0644]
hermes/lib/base.php [new file with mode: 0644]
hermes/lib/version.php [new file with mode: 0644]
hermes/locale/de_DE/LC_MESSAGES/hermes.mo [new file with mode: 0644]
hermes/locale/en_US/help.xml [new file with mode: 0644]
hermes/locale/es_ES/LC_MESSAGES/hermes.mo [new file with mode: 0644]
hermes/locale/es_ES/help.xml [new file with mode: 0644]
hermes/locale/fi_FI/LC_MESSAGES/hermes.mo [new file with mode: 0644]
hermes/locale/zh_TW/LC_MESSAGES/hermes.mo [new file with mode: 0644]
hermes/po/.cvsignore [new file with mode: 0644]
hermes/po/README [new file with mode: 0644]
hermes/po/de_DE.po [new file with mode: 0644]
hermes/po/es_ES.po [new file with mode: 0644]
hermes/po/fi_FI.po [new file with mode: 0644]
hermes/po/hermes.pot [new file with mode: 0644]
hermes/po/zh_TW.po [new file with mode: 0644]
hermes/scripts/.htaccess [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/Default.png [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/Icon.png [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/Info.plist [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/KeychainPlugIn.widgetplugin/Contents/Info.plist [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/KeychainPlugIn.widgetplugin/Contents/MacOS/KeychainPlugIn [new file with mode: 0755]
hermes/scripts/Sandals.wdgt/KeychainPlugIn.widgetplugin/Contents/Resources/English.lproj/InfoPlist.strings [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/Sandals.html [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/lib/Sandals.js [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/lib/horde.js [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/lib/open_calendar.js [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/lib/stripe.js [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/lib/vcXMLRPC.js [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/themes/bluewhite-screen.css [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/themes/graphics/alerts/alarm.png [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/themes/graphics/alerts/error.png [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/themes/graphics/alerts/message.png [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/themes/graphics/alerts/success.png [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/themes/graphics/alerts/warning.png [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/themes/graphics/calendar.png [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/themes/graphics/hermes.png [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/themes/graphics/required.png [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/themes/graphics/spinner-transparent.gif [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/themes/hermes-screen.css [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/themes/sandals-screen.css [new file with mode: 0644]
hermes/scripts/Sandals.wdgt/themes/screen.css [new file with mode: 0644]
hermes/scripts/purge.php [new file with mode: 0755]
hermes/scripts/sql/hermes.mssql.sql [new file with mode: 0644]
hermes/scripts/sql/hermes.oci8.sql [new file with mode: 0644]
hermes/scripts/sql/hermes.sql [new file with mode: 0644]
hermes/scripts/sql/hermes.xml [new file with mode: 0644]
hermes/scripts/upgrades/2007-04-20_drop_invoicing.sql [new file with mode: 0644]
hermes/scripts/upgrades/2007-04-25_add_jobtype_rate.sql [new file with mode: 0644]
hermes/scripts/upgrades/2007-10-17_add_jobtype_billable.sql [new file with mode: 0644]
hermes/scripts/upgrades/2008-07-01_deliverables_index.sql [new file with mode: 0644]
hermes/search.php [new file with mode: 0644]
hermes/start.php [new file with mode: 0644]
hermes/templates/common-header.inc [new file with mode: 0644]
hermes/templates/deliverables/list.inc [new file with mode: 0644]
hermes/templates/menu.inc [new file with mode: 0644]
hermes/templates/time/form.html [new file with mode: 0644]
hermes/themes/graphics/clockout.png [new file with mode: 0644]
hermes/themes/graphics/deliverable.png [new file with mode: 0644]
hermes/themes/graphics/favicon.ico [new file with mode: 0644]
hermes/themes/graphics/hermes.png [new file with mode: 0644]
hermes/themes/graphics/invoices.png [new file with mode: 0644]
hermes/themes/graphics/newdeliverable.png [new file with mode: 0644]
hermes/themes/graphics/timer-start.png [new file with mode: 0644]
hermes/themes/graphics/timer-stop.png [new file with mode: 0644]
hermes/themes/print/screen.css [new file with mode: 0644]
hermes/themes/screen.css [new file with mode: 0644]
hermes/time.php [new file with mode: 0644]

diff --git a/hermes/LICENSE b/hermes/LICENSE
new file mode 100644 (file)
index 0000000..07b15e9
--- /dev/null
@@ -0,0 +1,24 @@
+Copyright 2002-2009 The Horde Project. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ - Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+ - Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE HORDE PROJECT
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/hermes/README b/hermes/README
new file mode 100644 (file)
index 0000000..5656935
--- /dev/null
@@ -0,0 +1,138 @@
+What is Hermes?
+===============
+
+:Last update:   $Date: 2008/06/30 08:26:20 $
+:Revision:      $Revision: 1.10 $
+
+.. contents:: Contents
+.. section-numbering::
+
+Hermes is a Horde time-tracking application. It ties into address books (to
+retrieve clients) and task lists, bug trackers etc. (to retrieve cost
+objects). It comes with a stop watch, search and reporting capabilities, a
+MacOSX Dashboard widget and an invoice interface.
+
+This software is OSI Certified Open Source Software. OSI Certified is a
+certification mark of the `Open Source Initiative`_.
+
+.. _`Open Source Initiative`: http://www.opensource.org/
+
+
+Obtaining Hermes
+----------------
+
+Further information on Hermes and the latest version can be obtained at
+
+  http://www.horde.org/hermes/
+
+
+Documentation
+-------------
+
+The following documentation is available in the Hermes distribution:
+
+:README_:            This file
+:LICENSE_:           Copyright and license information
+:`docs/CHANGES`_:    Changes by release
+:`docs/CREDITS`_:    Project developers
+:`docs/INSTALL`_:    Installation instructions and notes
+:`docs/TODO`_:       Development TODO list
+
+
+Installation
+------------
+
+Instructions for installing Hermes can be found in the file INSTALL_ in the
+``docs/`` directory of the Hermes distribution.
+
+
+Configuration
+-------------
+
+Editing Submitted Time
+~~~~~~~~~~~~~~~~~~~~~~
+
+By default, users cannot edit submitted time.  Specific users or groups can be
+granted the ability to edit submitted (but not exported) time by granting the
+"EDIT" permission for "Hermes -> Time Review Screen" (see "Permissions" from
+the Administration menu for more info).  This will enable them to edit
+submitted time from anywhere they can see it, which right means from the
+Search screen.
+
+This permission also gives the user the ability to mark time as exported when
+downloading it.
+
+Cost Objects
+~~~~~~~~~~~~
+
+Other applications can supply cost objects to track time against.
+
+Currently, Whups_ (the ticket-tracking system) will export its tickets as
+possible cost object. If you configure an additional attribute for your ticket
+types and make its name "Estimated Time", Whups will also be able to export
+estimates on the tickets, allowing Hermes to indicate the ticket's percentage
+complete. The same happens automatically with tasks exported from Nag_ as cost
+objects.
+
+.. _Whups: http://www.horde.org/whups/
+.. _Nag: http://www.horde.org/nag/
+
+
+Using the OS X Widget
+---------------------
+
+If you are running Apple's OS X version 10.4 (Tiger) or later you can use the
+included widget, Sandals, to enter time directly from the Dashboard. Sandals
+can be found in the scripts/ directory. The easiest way to install it is to
+create a .zip of the directory and copy that to the OS X computer. After
+decompressing the .zip on the Mac, double-click the widget icon and it will be
+installed into your Dashboard.
+
+To use Sandals you must configure the Horde URL to rpc.php along with your
+username and password. Horde's rpc.php can be found in the base directory of
+your Horde install. Example: http://www.example.com/horde/rpc.php
+
+A note about security: If you are using OS X 10.5 (Leopard) your username and
+password will be securely stored in your Keychain. For 10.4 the credentials
+are stored in the normal Dashboard preferences unencrypted. If this is an
+issue for you, you should not use Sandals on OS X 10.4.
+
+
+Assistance
+----------
+
+If you encounter problems with Hermes, help is available!
+
+The Horde Frequently Asked Questions List (FAQ), available on the Web at
+
+  http://www.horde.org/faq/
+
+The Horde Project runs a number of mailing lists, for individual applications
+and for issues relating to the project as a whole. Information, archives, and
+subscription information can be found at
+
+  http://www.horde.org/mail/
+
+Lastly, Horde developers, contributors and users also make occasional
+appearances on IRC, on the channel #horde on the freenode Network
+(irc.freenode.net).
+
+
+Licensing
+---------
+
+For licensing and copyright information, please see the file LICENSE_ in the
+Hermes distribution.
+
+Thanks,
+
+The Hermes team
+
+
+.. _README: ?f=README.html
+.. _LICENSE: http://www.horde.org/licenses/asl.php
+.. _docs/CHANGES: ?f=CHANGES.html
+.. _docs/CREDITS: ?f=CREDITS.html
+.. _INSTALL:
+.. _docs/INSTALL: ?f=INSTALL.html
+.. _docs/TODO: ?f=TODO.html
diff --git a/hermes/admin.php b/hermes/admin.php
new file mode 100644 (file)
index 0000000..840f93b
--- /dev/null
@@ -0,0 +1,251 @@
+<?php
+/**
+ * $Horde: hermes/admin.php,v 1.33 2009/07/14 18:43:47 selsky Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ *
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ */
+
+@define('HERMES_BASE', dirname(__FILE__));
+require_once HERMES_BASE . '/lib/base.php';
+require_once HERMES_BASE . '/lib/Admin.php';
+
+if (!Horde_Auth::isAdmin()) {
+    exit('forbidden.');
+}
+
+$r = new Horde_Form_Renderer();
+$vars = Horde_Variables::getDefaultVariables();
+$beendone = false;
+
+function _open()
+{
+    static $opened;
+
+    if (is_null($opened)) {
+        global $registry, $prefs, $browser, $conf, $notification, $beendone, $title;
+
+        $opened = true;
+        $beendone = true;
+        $title = _("Administration");
+        require HERMES_TEMPLATES . '/common-header.inc';
+        require HERMES_TEMPLATES . '/menu.inc';
+    }
+}
+
+if ($vars->exists('formname')) {
+    switch ($vars->get('formname')) {
+    case 'addjobtypeform':
+        $form = new AddJobTypeForm($vars);
+        $form->validate($vars);
+
+        if ($form->isValid()) {
+            $form->getInfo($vars, $info);
+            $result = $hermes->updateJobType($info);
+            if (!is_a($result, 'PEAR_Error')) {
+                $notification->push(sprintf(_("The job type \"%s\" has been added."), $vars->get('name')), 'horde.success');
+            } else {
+                $notification->push(sprintf(_("There was an error adding the job type: %s."), $result->getMessage()), 'horde.error');
+            }
+        } else {
+            _open();
+
+            $form->open($r, $vars, 'admin.php', 'post');
+            $r->beginActive(_("Add Job Type"));
+            $r->renderFormActive($form, $vars);
+            $r->submit();
+            $r->end();
+            $form->close($r);
+        }
+        break;
+
+    case 'editjobtypestep1form':
+        $form1 = new EditJobTypeStep1Form($vars);
+        $form1->validate($vars);
+
+        _open();
+
+        if ($form1->isValid()) {
+            switch ($vars->get('submitbutton')) {
+            case _("Edit Job Type"):
+                $form2 = new EditJobTypeStep2Form($vars);
+                $form2->open($r, $vars, 'admin.php', 'post');
+
+                // render the second stage form
+                $r->beginActive(_("Edit Job Type, Step 2"));
+                $r->renderFormActive($form2, $vars);
+                $r->submit();
+                $r->end();
+
+                $form2->close($r);
+                break;
+
+            case _("Delete Job Type"):
+                $form2 = new DeleteJobTypeForm($vars);
+                $form2->open($r, $vars, 'admin.php', 'post');
+
+                // render the deletion form
+                $r->beginActive(_("Delete Job Type: Confirmation"));
+                $r->renderFormActive($form2, $vars);
+                $r->submit();
+                $r->end();
+
+                $form2->close($r);
+                break;
+            }
+        } else {
+            $form1->open($r, $vars, 'admin.php', 'post');
+            $r->beginActive(_("Edit job type"));
+            $r->renderFormActive($form1, $vars);
+            $r->submit();
+            $r->end();
+            $form1->close($r);
+        }
+        break;
+
+    case 'editclientstep1form':
+        $form1 = new EditClientStep1Form($vars);
+        $form1->validate($vars);
+
+        _open();
+
+        if ($form1->isValid()) {
+            $form2 = new EditClientStep2Form($vars);
+            $form2->open($r, $vars, 'admin.php', 'post');
+
+            // render the second stage form
+            $r->beginActive(_("Edit Client Settings, Step 2"));
+            $r->renderFormActive($form2, $vars);
+            $r->submit();
+            $r->end();
+
+            $form2->close($r);
+        } else {
+            $form1->open($r, $vars, 'admin.php', 'post');
+            $r->beginActive(_("Edit Client Settings"));
+            $r->renderFormActive($form1, $vars);
+            $r->submit();
+            $r->end();
+            $form1->close($r);
+        }
+        break;
+
+    case 'editjobtypestep2form':
+        $form1 = new EditJobTypeStep2Form($vars);
+        $form1->validate($vars);
+
+        if ($form1->isValid()) {
+            // update everything.
+            $form1->getInfo($vars, $info);
+            $info['id'] = $info['jobtype'];
+            $result = $hermes->updateJobType($info);
+            if (!PEAR::isError($result)) {
+                $notification->push(_("The job type has been modified."), 'horde.success');
+            } else {
+                $notification->push(sprintf(_("There was an error editing the job type: %s."), $result->getMessage()), 'horde.error');
+            }
+        } else {
+            _open();
+
+            $form1->open($r, $vars, 'admin.php', 'post');
+            $r->beginActive(_("Edit job type, Step 2"));
+            $r->renderFormActive($form1, $vars);
+            $r->submit();
+            $r->end();
+            $form1->close($r);
+        }
+        break;
+
+    case 'editclientstep2form':
+        $form = new EditClientStep2Form($vars);
+        $form->validate($vars);
+
+        if ($form->isValid()) {
+            $result = $hermes->updateClientSettings($vars->get('client'),
+                                                    $vars->get('enterdescription') ? 1 : 0,
+                                                    $vars->get('exportid'));
+            if (PEAR::isError($result)) {
+                $notification->push(sprintf(_("There was an error editing the client settings: %s."), $result->getMessage()), 'horde.error');
+            } else {
+                $notification->push(_("The client settings have been modified."), 'horde.success');
+            }
+        } else {
+            _open();
+
+            $form->open($r, $vars, 'admin.php', 'post');
+            $r->beginActive(_("Edit Client Settings, Step 2"));
+            $r->renderFormActive($form, $vars);
+            $r->submit();
+            $r->end();
+            $form->close($r);
+        }
+        break;
+
+    case 'deletejobtypeform':
+        $form = new DeleteJobTypeForm($vars);
+        $form->validate($vars);
+
+        if ($form->isValid()) {
+            if ($vars->get('yesno') == 1) {
+                $result = $hermes->deleteJobType($vars->get('jobtype'));
+                if (!PEAR::isError($result)) {
+                    $notification->push(_("The job type has been deleted."), 'horde.success');
+                } else {
+                    $notification->push(sprintf(_("There was an error deleting the job type: %s."), $result->getMessage()), 'horde.error');
+                }
+            } else {
+                $notification->push(_("The job type was not deleted."), 'horde.message');
+            }
+        } else {
+            _open();
+
+            $form->open($r, $vars, 'admin.php', 'post');
+            $r->beginActive(_("Delete Job Type: Confirmation"));
+            $r->renderFormActive($form, $vars);
+            $r->submit();
+            $r->end();
+            $form->close($r);
+        }
+        break;
+    }
+}
+
+if (!$beendone) {
+    $vars = new Horde_Variables();
+    $form1 = new EditJobTypeStep1Form($vars); $edit1 = _("Edit Job Type"); $edit2 = _("Delete Job Type");
+    $form2 = new AddJobTypeForm($vars); $add = _("Add Job Type");
+    $form3 = new EditClientStep1Form($vars); $edit3 = _("Edit Client Settings");
+
+    _open();
+
+    $form1->open($r, $vars, 'admin.php', 'post');
+    $r->beginActive($edit1);
+    $r->renderFormActive($form1, $vars);
+    $r->submit(array($edit1, $edit2));
+    $r->end();
+    $form1->close($r);
+
+    echo '<br />';
+
+    $form2->open($r, $vars, 'admin.php', 'post');
+    $r->beginActive($add);
+    $r->renderFormActive($form2, $vars);
+    $r->submit($add);
+    $r->end();
+    $form2->close($r);
+
+    echo '<br />';
+
+    $form3->open($r, $vars, 'admin.php', 'post');
+    $r->beginActive($edit3);
+    $r->renderFormActive($form3, $vars);
+    $r->submit($edit3);
+    $r->end();
+    $form3->close($r);
+}
+
+require $registry->get('templates', 'horde') . '/common-footer.inc';
diff --git a/hermes/config/.cvsignore b/hermes/config/.cvsignore
new file mode 100644 (file)
index 0000000..51adefa
--- /dev/null
@@ -0,0 +1,3 @@
+conf.php
+conf.bak.php
+prefs.php
diff --git a/hermes/config/.htaccess b/hermes/config/.htaccess
new file mode 100644 (file)
index 0000000..3a42882
--- /dev/null
@@ -0,0 +1 @@
+Deny from all
diff --git a/hermes/config/conf.xml b/hermes/config/conf.xml
new file mode 100644 (file)
index 0000000..2e527fc
--- /dev/null
@@ -0,0 +1,64 @@
+<?xml version="1.0"?>
+<!-- $Horde: hermes/config/conf.xml,v 1.21 2007/09/13 18:16:11 jan Exp $ -->
+<configuration>
+ <configsection name="time">
+  <configheader>Time Tracking Settings</configheader>
+  <configboolean name="choose_ifbillable" desc="Should users enter whether
+  time is billable or not billable?">true</configboolean>
+  <configinteger name="days_to_keep" desc="How many days should we keep time
+  after it is exported/billed?">60</configinteger>
+  <configboolean name="deliverables" desc="Enable deliverables as cost
+  objects?">false</configboolean>
+  <configboolean name="sum_billable_only" desc="Only include billable times
+  when calculating time sums?">false</configboolean>
+ </configsection>
+
+ <configsection name="storage">
+  <configheader>Storage System Settings</configheader>
+  <configenum name="driver" desc="What storage driver should we use?">sql
+   <values>
+    <value>sql</value>
+   </values>
+  </configenum>
+  <configsection name="params">
+   <configsql switchname="driverconfig"/>
+  </configsection>
+ </configsection>
+
+ <configsection name="client">
+  <configheader>Clients</configheader>
+  <configstring name="field" desc="Field name from the client address book to
+  display in the client list">name</configstring>
+ </configsection>
+
+ <configsection name="invoices">
+  <configheader>Invoices</configheader>
+  <configswitch name="driver" desc="What invoices driver should we use?">false
+   <case name="false" desc="false"/>
+   <case name="minerva" desc="Minerva">
+    <configsection name="params">
+     <configstring name="type" desc="Type of created
+     invoices">proforma</configstring>
+     <configstring name="status" desc="Status of created
+     invoices">pending</configstring>
+     <configstring name="expire" desc="Due of created
+     invoices">8</configstring>
+     <configstring name="place" desc="Place of created invoices">City of
+     Internet</configstring>
+    </configsection>
+   </case>
+  </configswitch>
+ </configsection>
+
+ <configsection name="menu">
+  <configheader>Menu Settings</configheader>
+  <configboolean name="print" desc="Should we display a Print link in the
+  main menu?">true</configboolean>
+  <configmultienum name="apps" desc="Select any applications that should be
+  linked in Hermes' menu">
+   <values>
+    <configspecial name="list-horde-apps" />
+   </values>
+  </configmultienum>
+ </configsection>
+</configuration>
diff --git a/hermes/config/prefs.php.dist b/hermes/config/prefs.php.dist
new file mode 100644 (file)
index 0000000..e27d551
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+/**
+ * $Horde: hermes/config/prefs.php.dist,v 1.9 2008/06/25 16:05:34 jan Exp $
+ *
+ * See horde/config/prefs.php for documentation on the structure of this file.
+ */
+
+$prefGroups['timer'] = array(
+    'column' => _("General Options"),
+    'label' => _("Timer Options"),
+    'desc' => _("Set preferences on the stop watch timer."),
+    'members' => array('add_description')
+);
+
+// should we add the stop watch name to the description?
+$_prefs['add_description'] = array(
+    'value' => true,
+    'locked' => false,
+    'shared' => false,
+    'type' => 'checkbox',
+    'desc' => _("Add stop watch name and start and end time to the description of the time entry?")
+);
+
+// preference for holding any running timers.
+$_prefs['running_timers'] = array(
+    'value' => '',
+    'locked' => false,
+    'shared' => false,
+    'type' => 'implicit'
+);
diff --git a/hermes/deliverables.php b/hermes/deliverables.php
new file mode 100644 (file)
index 0000000..11cd6f0
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+/**
+ * $Horde: hermes/deliverables.php,v 1.14 2009/07/14 18:43:47 selsky Exp $
+ *
+ * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ *
+ * @author Jason M. Felice <jason.m.felice@gmail.com>
+ */
+
+@define('HERMES_BASE', dirname(__FILE__));
+require_once HERMES_BASE . '/lib/base.php';
+require_once HERMES_BASE . '/lib/Forms/Deliverable.php';
+
+$vars = Horde_Variables::getDefaultVariables();
+
+switch ($vars->get('formname')) {
+case 'deliverableform':
+    $form = new DeliverableForm($vars);
+    $form->validate($vars);
+    if ($form->isValid()) {
+        $form->getInfo($vars, $info);
+        if (!empty($info['deliverable_id'])) {
+            $info['id'] = $info['deliverable_id'];
+            if (empty($info['parent'])) {
+                $origdeliv = $hermes->getDeliverableByID($info['id']);
+                if (!is_a($origdeliv, 'PEAR_Error')) {
+                    $info['parent'] = $origdeliv['parent'];
+                }
+            }
+        }
+        $res = $hermes->updateDeliverable($info);
+        if (is_a($res, 'PEAR_Error')) {
+            $notification->push(sprintf(_("Error saving deliverable: %s"),
+                                        $res->getMessage()),
+                                'horde.error');
+        } else {
+            $notification->push(_("Deliverable saved successfully."),
+                                'horde.success');
+            $vars = new Horde_Variables(array('client_id' => $vars->get('client_id')));
+        }
+    }
+    break;
+
+case 'deletedeliverable':
+    $res = $hermes->deleteDeliverable($vars->get('delete'));
+    if (is_a($res, 'PEAR_Error')) {
+        $notification->push(sprintf(_("Error deleting deliverable: %s"),
+                                    $res->getMessage()), 'horde.error');
+    } else {
+        $notification->push(_("Deliverable successfully deleted."),
+                            'horde.success');
+    }
+    break;
+}
+
+$title = _("Deliverables");
+require HERMES_TEMPLATES . '/common-header.inc';
+require HERMES_TEMPLATES . '/menu.inc';
+
+$renderer = new Horde_Form_Renderer();
+
+if (!$vars->exists('deliverable_id') && !$vars->exists('new')) {
+    $clientSelector = new DeliverableClientSelector($vars);
+    $clientSelector->renderActive($renderer, $vars, 'deliverables.php', 'post');
+}
+
+if ($vars->exists('deliverable_id') || $vars->exists('new')) {
+    if ($vars->exists('deliverable_id')) {
+        $deliverable = $hermes->getDeliverableByID($vars->get('deliverable_id'));
+        if (is_a($deliverable, 'PEAR_Error')) {
+            Horde::fatal($deliverable, __FILE__, __LINE__);
+        }
+
+        foreach ($deliverable as $name => $value) {
+            $vars->set($name, $value);
+        }
+    }
+
+    $form = new DeliverableForm($vars);
+    $form->renderActive($renderer, $vars, 'deliverables.php', 'post');
+} elseif ($vars->exists('client_id')) {
+    $clients = Hermes::listClients();
+    $clientname = $clients[$vars->get('client_id')];
+
+    $deliverables = $hermes->listDeliverables(array('client_id' => $vars->get('client_id')));
+    if (is_a($deliverables, 'PEAR_Error')) {
+        Horde::fatal($deliverables, __FILE__, __LINE__);
+    }
+
+    $tree = Horde_Tree::factory('deliverables', 'javascript');
+    $tree->setOption(array('class'       => 'item',
+                           'alternate'   => true));
+
+    foreach ($deliverables as $deliverable) {
+        $params = array();
+        $params['url'] = Horde::applicationUrl('deliverables.php');
+        $params['url'] = Horde_Util::addParameter($params['url'], array('deliverable_id' => $deliverable['id'], 'client_id' => $vars->get('client_id')));
+        $params['title'] = sprintf(_("Edit %s"), $deliverable['name']);
+
+        $newdeliv = '&nbsp;' . Horde::link(Horde_Util::addParameter(Horde::applicationUrl('deliverables.php'), array('new' => 1, 'parent' => $deliverable['id'], 'client_id' => $vars->get('client_id'))), _("New Sub-deliverable")) . Horde::img('newdeliverable.png', _("New Sub-deliverable")) . '</a>';
+
+        $deldeliv = '&nbsp;' . Horde::link(Horde_Util::addParameter(Horde::applicationUrl('deliverables.php'), array('formname' => 'deletedeliverable', 'delete' => $deliverable['id'], 'client_id' => $vars->get('client_id'))), _("Delete This Deliverable")) . Horde::img('delete.png', _("Delete This Deliverable"), '', $registry->getImageDir('horde')) . '</a>';
+
+        /* Calculate the node's depth. */
+        $depth = 0;
+        $iterator = $deliverable;
+        while (!empty($iterator['parent'])) {
+            $depth++;
+            $iterator = $deliverables[$iterator['parent']];
+        }
+
+        $tree->addNode($deliverable['id'], $deliverable['parent'],
+                       $deliverable['name'], $depth, true, $params,
+                       array($newdeliv, $deldeliv), array());
+    }
+
+    require HERMES_TEMPLATES . '/deliverables/list.inc';
+}
+
+require $registry->get('templates', 'horde') . '/common-footer.inc';
diff --git a/hermes/docs/CHANGES b/hermes/docs/CHANGES
new file mode 100644 (file)
index 0000000..3417ffc
--- /dev/null
@@ -0,0 +1,91 @@
+--------
+v2.0-cvs
+--------
+
+
+
+
+----------
+v1.0.2-cvs
+----------
+
+
+
+
+------
+v1.0.1
+------
+
+[jan] Fix client list in search form if client source has numeric IDs
+      (manilal@ejyothi.com, Bug #7966).
+
+
+----
+v1.0
+----
+
+[jan] Go back to search results after editing time slices from there
+      (mail@dunix-data.de, Request #7012).    
+[jan] Check permissions when displaying time slice forms (mail@dunix-data.de,
+      Bug #7027).
+[jan] Fix index for table hermes_deliverables (Bug #7001).
+
+
+--------
+v1.0-RC1
+--------
+
+[jan] Add start and end time of stop watch to entry description (Request #5585).
+[jan] Update time budget of cost objects after saving an entry.
+[bak] Add OS X widget for entering time
+[jan] Allow to mark job types billable (Vinay Kumar <vinay.kumar@ejyothi.com>,
+      Request #5207).
+[jan] Add configuration to only include billable time when summing up for
+      deliverables.
+[cjh] Add an initial interface for sending invoice data to Minerva
+      (Duck <duck@obala.net).
+[jan] Add hourly rates to job types (Duck <duck@obala.net>).
+[jan] Drop incomplete invoicing support.
+[jan] Add configuration for address book field used for the client listings.
+      (Request #3816).
+[jan] Add "Enter Time" item to sidebar menu.
+[ben] Better support for MS-SQL.
+[jan] Add subtotal tabs to the overview page.
+[jan] Save used stop watch name in "Notes" field (tevans@tachometry.com,
+      Request #2667).
+[cjh] Add subtotal tabs to the time search form
+      (tevans@tachometry.com, Bug #2632).
+[cjh] Add cost object filtering to the time search page
+      (tevans@tachometry.com, Bug #2631).
+[cjh] Print button for timesheets, and support for only showing some timesheet
+      elements when printing (Bug #2375).
+[jmf] Replace support tracking time to deliverables with support for tracking
+      time to more general cost objects which can be provided by other
+      applications.  Deliverables are now a type of cost object.
+[jan] Add stop watch to sidebar.
+[cjh] Use bind variables in SQL driver (selsky@columbia.edu, #1745).
+[cjh] Use client name instead of id in exports, and allow searching on
+      whether or not time is billable (Tom Evans <tevans@tachometry.com>).
+[jmf] Add ability to track time to deliverables, maintain deliverables.
+[jmf] Add ability to disable job types so they still exist but can no longer
+      be used for time entry (but can be used for searching and display).
+[jan] Add Spanish translation (Manuel Perez Ayala <mperaya@alcazaba.unex.es>).
+[jmf] Merge submit screen into time entry screen.
+[jmf] Make time entry form "float".
+[jmf] Remove concept of "current" week.
+[jan] Add Finnish translation (Petteri Karttunen <pkarttun@siba.fi>).
+[jan] Add Traditional Chinese translation (David Chang <david@thbuo.gov.tw>).
+[cjh] Add a print menu item.
+[cjh] Add a script to purge old data.
+[cjh] Fix adding of bogus data because of other form variables.
+[jan] Add German translation.
+[cjh] Add browsing to arbitrary weeks.
+[cjh] Only submitted time gets exported now.
+[cjh] Add submitting of time.
+[cjh] Add editing of time.
+[cjh] Exporting hours by date range works now.
+[cjh] Initial support for exporting hours to QuickBooks.
+[cjh] There is now an Options page.
+[cjh] Users can now edit time that hasn't yet been sucked into a timesheet.
+[cjh] Add an admin interface for Job Types.
+[cjh] Initial Hermes framework.
diff --git a/hermes/docs/CREDITS b/hermes/docs/CREDITS
new file mode 100644 (file)
index 0000000..10d4a6e
--- /dev/null
@@ -0,0 +1,27 @@
+=========================
+ Hermes Development Team
+=========================
+
+
+Core Developers
+===============
+
+Chuck Hagenbuch <chuck@horde.org>
+ - Initial code, sponsored by Hawk Technologies (http://www.hawk.com/).
+
+Jan Schneider <jan@horde.org>
+ - Maintenance, stuff.
+
+Ben Klang <ben@alkaloid.net>
+ - Hermes API and Dashboard Widget.
+
+
+Localization
+============
+
+=====================   ======================================================
+Chinese (Traditional)   David Chang <david@tmv.gov.tw>
+Finnish                 Petteri Karttunen <pkarttun@siba.fi>
+German                  Jan Schneider <jan@horde.org>
+Spanish                 Manuel Perez Ayala <mperaya@alcazaba.unex.es>
+=====================   ======================================================
diff --git a/hermes/docs/INSTALL b/hermes/docs/INSTALL
new file mode 100644 (file)
index 0000000..f0fb6d4
--- /dev/null
@@ -0,0 +1,243 @@
+=======================
+ Installing Hermes 1.0
+=======================
+
+:Last update:   $Date: 2008/06/29 22:03:03 $
+:Revision:      $Revision: 1.13 $
+
+.. contents:: Contents
+.. section-numbering::
+
+This document contains instructions for installing the Hermes time-tracking
+application.
+
+For information on the capabilities and features of Hermes, see the file
+README_ in the top-level directory of the Hermes distribution.
+
+
+Obtaining Hermes
+================
+
+Hermes can be obtained from the Horde website and FTP server, at
+
+   http://www.horde.org/hermes/
+
+   ftp://ftp.horde.org/pub/hermes/
+
+Or use the mirror closest to you:
+
+   http://www.horde.org/mirrors.php
+
+Bleeding-edge development versions of Hermes are available via CVS; see the
+file `horde/docs/HACKING`_ in the Horde distribution, or the website
+http://www.horde.org/source/, for information on accessing the Horde CVS
+repository.
+
+
+Prerequisites
+=============
+
+To function properly, Hermes **requires** the following:
+
+1. A working Horde installation.
+
+   Hermes runs within the `Horde Application Framework`_, a set of common
+   tools for Web applications written in PHP. You must install Horde before
+   installing Hermes.
+
+   .. Important:: Hermes 1.0 requires version 3.2+ of the Horde Framework -
+                  earlier versions of Horde will **not** work.
+
+   .. _`Horde Application Framework`: http://www.horde.org/horde/
+
+   The Horde Framework can be obtained from the Horde website and FTP server,
+   at
+
+      http://www.horde.org/horde/
+
+      ftp://ftp.horde.org/pub/horde/
+
+   Many of Hermes's prerequisites are also Horde prerequisites.
+
+   .. Important:: Be sure to have completed all of the steps in the
+                  `horde/docs/INSTALL`_ file for the Horde Framework before
+                  installing Hermes.
+
+2. The following PHP capabilities:
+
+   a. SQL support
+
+      Hermes stores its data in an SQL database. Build PHP with whichever SQL
+      driver you require; see the `horde/docs/INSTALL`_ file for details.
+
+3. A working Turba installation.
+
+   Hermes queries the clients API for client listings. This API is usually
+   provided by Turba_. It doesn't matter what type of address book is
+   configured for clients, but it is a good practice to create one which is
+   separate from the rest of your contacts, see
+   http://wiki.horde.org/HermesAddressBook.
+
+   Which addressbook Hermes uses for clients is configured in Turba's setup.
+
+   You can replace Turba with any application providing the
+   clients/listClients API method. Check the `Horde Wiki`_ or ask on the
+   `mailing list`_ for details.
+
+.. _Turba: http://www.horde.org/turba/
+.. _`Horde Wiki`: http://wiki.horde.org/
+.. _`mailing list`: `Obtaining Support`_
+
+The following items are not required, but are recommended:
+
+1. Applications providing cost objects.
+
+   Other applications can supply cost objects to track time against.
+
+   Currently, Whups_ (the ticket-tracking system) and Nag_ (the tasklist
+   manager) will export their tickets as possible cost objects. If you
+   configure an additional attribute for your ticket types in Whups and make
+   its name "Estimated Time", Whups will also be able to export estimates on
+   the tickets, allowing Hermes to indicate the ticket's percentage
+   complete. Estimation times from Nag will be exported automatically too.
+
+.. _Whups: http://www.horde.org/whups/
+.. _Nag: http://www.horde.org/nag/
+
+
+Installing Hermes
+===================
+
+Hermes is written in PHP, and must be installed in a web-accessible
+directory. The precise location of this directory will differ from system to
+system. Conventionally, Hermes is installed directly underneath Horde in the
+web server's document tree.
+
+Since Hermes is written in PHP, there is no compilation necessary; simply
+expand the distribution where you want it to reside and rename the root
+directory of the distribution to whatever you wish to appear in the URL. For
+example, with the Apache web server's default document root of
+``/usr/local/apache/htdocs``, you would type::
+
+   cd /usr/local/apache/htdocs/horde
+   tar zxvf /path/to/hermes-h3-x.y.z.tar.gz
+   mv hermes-h3-x.y.z hermes
+
+and would then find Hermes at the URL::
+
+   http://your-server/horde/hermes/
+
+
+Configuring Hermes
+==================
+
+1. Configuring Horde for Hermes
+
+   a. Register the application
+
+      In ``horde/config/registry.php``, find the ``applications['hermes']``
+      stanza. The default settings here should be okay, but you can change
+      them if desired. If you have changed the location of Hermes relative to
+      Horde, either in the URL, in the filesystem or both, you must update the
+      ``fileroot`` and ``webroot`` settings to their correct values.
+
+2. Creating the database tables
+
+   The specific steps to create Hermes's database tables depend on which
+   database you've chosen to use.
+
+   First, look in ``scripts/sql/`` to see if a script already exists for your
+   database type. If so, you should be able to simply execute that script as
+   superuser in your database. (Note that executing the script as the "horde"
+   user will probably fail when granting privileges.)
+
+   If such a script does not exist, you'll need to build your own, using the
+   file ``hermes.sql`` as a starting point.  If you need assistance in
+   creating database tables, you may wish to let us know on the Hermes mailing
+   list.
+
+   You will also need to make sure that the "horde" user in your database has
+   table-creation privileges, so that the tables that `PEAR DB`_ uses to
+   provide portable sequences can be created.
+
+   .. _`PEAR DB`: http://pear.php.net/DB
+
+3. Configuring Hermes
+
+   To configure Hermes, change to the ``config/`` directory of the installed
+   distribution, and make copies of all of the configuration ``dist`` files
+   without the ``dist`` suffix::
+
+      cd config/
+      for foo in *.dist; do cp $foo `basename $foo .dist`; done
+
+   Or on Windows::
+
+      copy *.dist *.
+
+   Documentation on the format and purpose of those files can be found in each
+   file. You may edit these files if you wish to customize Hermes's appearance
+   and behavior.
+
+   You must login to Horde as a Horde Administrator to finish the
+   configuration of Hermes. Use the Horde ``Administration`` menu item to get
+   to the administration page, and then click on the ``Configuration`` icon to
+   get the configuration page. Select ``Time Tracking`` from the selection
+   list of applications. Fill in or change any configuration values as
+   needed. When done click on ``Generate Time Tracking Configuration`` to
+   generate the ``conf.php`` file. If your web server doesn't have write
+   permissions to the Hermes configuration directory or file, it will not be
+   able to write the file. In this case, go back to ``Configuration`` and
+   choose one of the other methods to create the configuration file
+   ``hermes/config/conf.php``.
+
+   Note for international users: Hermes uses GNU gettext to provide local
+   translations of text displayed by applications; the translations are found
+   in the ``po/`` directory. If a translation is not yet available for your
+   locale (and you wish to create one), see the ``horde/po/README`` file, or
+   if you're having trouble using a provided translation, please see the
+   `horde/docs/TRANSLATIONS`_ file for instructions.
+
+4. Testing Hermes
+
+   Use Hermes to enter time-tracking data. Test at least the following:
+
+   - Adding job types as an administrator.
+   - Adding deliverables as an administrator.
+   - Creating time entries for a client, job type, and deliverable.
+   - Search for time entries.
+   - Submit time entries.
+
+
+Obtaining Support
+=================
+
+If you encounter problems with Hermes, help is available!
+
+The Horde Frequently Asked Questions List (FAQ), available on the Web at
+
+  http://www.horde.org/faq/
+
+The Horde Project runs a number of mailing lists, for individual applications
+and for issues relating to the project as a whole. Information, archives, and
+subscription information can be found at
+
+  http://www.horde.org/mail/
+
+Lastly, Horde developers, contributors and users may also be found on IRC,
+on the channel #horde on the Freenode Network (irc.freenode.net).
+
+Please keep in mind that Hermes is free software written by volunteers.
+For information on reasonable support expectations, please read
+
+  http://www.horde.org/support.php
+
+Thanks for using Hermes!
+
+The Hermes team
+
+
+.. _README: ?f=README.html
+.. _`horde/docs/HACKING`: ../../horde/docs/?f=HACKING.html
+.. _`horde/docs/INSTALL`: ../../horde/docs/?f=INSTALL.html
+.. _`horde/docs/TRANSLATIONS`: ../../horde/docs/?f=TRANSLATIONS.html
diff --git a/hermes/docs/RELEASE_NOTES b/hermes/docs/RELEASE_NOTES
new file mode 100644 (file)
index 0000000..6b70a29
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+/**
+ * Release focus. Possible values:
+ * 0 - N/A
+ * 1 - Initial freshmeat announcement
+ * 2 - Documentation
+ * 3 - Code cleanup
+ * 4 - Minor feature enhancements
+ * 5 - Major feature enhancements
+ * 6 - Minor bugfixes
+ * 7 - Major bugfixes
+ * 8 - Minor security fixes
+ * 9 - Major security fixes
+ */
+$this->notes['fm']['focus'] = 4;
+
+/* Mailing list release notes. */
+$this->notes['ml']['changes'] = <<<ML
+The Horde Team is pleased to announce the second release candidate of the
+Hermes Time Tracking Application version H3 (1.0).
+
+Hermes is a time tracking and reporting application written for the Horde
+Framework. Taking advantage of the Framework for authentication and
+presentation, it integrates seamlessly with other Horde applications allowing
+users to track time against to do items (from Nag), tickets (from Whups), or
+pre-configured projects within Hermes itself. The list of billable clients is
+taken from the address book (Turba). Additionally, there is an OS X Widget to
+allow Mac users to enter time directly from his or her Dashboard.
+
+Barring any problems, this code will be released as Hermes H3 (1.0).
+Testing is requested and comments are encouraged.
+Updated translations would also be great.
+
+Major changes compared to the Hermes H3 (1.0-RC1) version are:
+    * Improved permissions checking.
+    * Added ability to return to search results after editing.
+ML;
+
+/* Freshmeat release notes, not more than 600 characters. */
+$this->notes['fm']['changes'] = <<<FM
+Improved permissions checking and the ability to return to search results after editing time slices have been added.
+FM;
+
+$this->notes['name'] = 'Hermes';
+$this->notes['list'] = 'horde';
+$this->notes['fm']['project'] = 'horde-hermes';
+$this->notes['fm']['branch'] = 'Default';
diff --git a/hermes/docs/TODO b/hermes/docs/TODO
new file mode 100644 (file)
index 0000000..7231886
--- /dev/null
@@ -0,0 +1,29 @@
+==============================
+ Hermes Development TODO List
+==============================
+
+:Last update:   $Date: 2008/06/25 17:20:56 $
+:Revision:      $Revision: 1.11 $
+
+- Add support for managing "time pools" such as vacation time and sick days.
+
+- Add a request and approval mechanism (for some time pools, such as
+  vacation).
+
+- Add employee expenses.  Allow them to be billed to clients.  Add search
+  and export ability for them.
+
+- Add "Don't know" to "Billable?" options.  A manager must review and decide
+  on all time before exporting.
+
+- Allow selection of which fields to display for clients.
+
+- Separate "review" permission into "Other People's Time" and "Submitted Time"
+  permissions (and "Other People's Submitted Time"?)
+
+- Preference to select a new entry mode where user can enter start time and
+  end time (instead of number of hours).
+
+- The client list can get quite large.  Keep the last ten MRU in the drop
+  down box, with a sixth option being "More...".  Javascript will pop open a
+  window with the full list (or a search window?) to cycle one into the MRU.
diff --git a/hermes/entry.php b/hermes/entry.php
new file mode 100644 (file)
index 0000000..d07eb1e
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+/**
+ * $Horde: hermes/entry.php,v 1.27 2009/07/08 18:29:07 slusarz Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ *
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ * @author Jan Schneider <jan@horde.org>
+ */
+
+@define('HERMES_BASE', dirname(__FILE__));
+require_once HERMES_BASE . '/lib/base.php';
+require_once HERMES_BASE . '/lib/Forms/Time.php';
+
+$vars = Horde_Variables::getDefaultVariables();
+
+if (!$vars->exists('id') && $vars->exists('timer')) {
+    $timer_id = $vars->get('timer');
+    $timers = @unserialize($prefs->getValue('running_timers', false));
+    if ($timers && isset($timers[$timer_id])) {
+        $tname = Horde_String::convertCharset($timers[$timer_id]['name'], $prefs->getCharset());
+        $tformat = $prefs->getValue('twentyFour') ? 'G:i' : 'g:i a';
+        $vars->set('hours', round((float)(time() - $timer_id) / 3600, 2));
+        if ($prefs->getValue('add_description')) {
+            $vars->set('note', sprintf(_("Using the \"%s\" stop watch from %s to %s"), $tname, date($tformat, $timer_id), date($tformat, time())));
+        }
+        $notification->push(sprintf(_("The stop watch \"%s\" has been stopped."), $tname), 'horde.success');
+        unset($timers[$timer_id]);
+        $prefs->setValue('running_timers', serialize($timers), false);
+    }
+}
+
+switch ($vars->get('formname')) {
+case 'timeentryform':
+    $form = new TimeEntryForm($vars);
+    if ($form->validate($vars)) {
+        $form->getInfo($vars, $info);
+        if ($vars->exists('id')) {
+            $msg = _("Your time was successfully updated.");
+            $result = $hermes->updateTime(array($info));
+            $do_redirect = true;
+        } else {
+            $msg = _("Your time was successfully entered.");
+            $result = $hermes->enterTime(Horde_Auth::getAuth(), $info);
+            $do_redirect = false;
+        }
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            $notification->push(sprintf(_("There was an error storing your timesheet: %s"), $result->getMessage()), 'horde.error');
+            $do_redirect = false;
+        } else {
+            $notification->push($msg, 'horde.success');
+        }
+        if ($do_redirect) {
+            $url = $vars->get('url');
+            if (empty($url)) {
+                $url = Horde::applicationUrl('time.php');
+            }
+            header('Location: ' . $url);
+            exit;
+        }
+    }
+    break;
+
+default:
+    if ($vars->exists('id')) {
+        // We are updating a specific entry, load it into the form variables.
+        $id = $vars->get('id');
+        if (!Hermes::canEditTimeslice($id)) {
+            $notification->push(_("Access denied; user cannot modify this timeslice."), 'horde.error');
+            header('Location: ' . Horde::applicationUrl('time.php'));
+            exit;
+        }
+        $myhours = $hermes->getHours(array('id' => $id));
+        if (is_array($myhours)) {
+            foreach ($myhours as $item) {
+                if (isset($item['id']) && $item['id'] == $id) {
+                    foreach ($item as $key => $value) {
+                        $vars->set($key, $value);
+                    }
+                }
+            }
+        }
+    }
+    $form = new TimeEntryForm($vars);
+    break;
+}
+$form->setCostObjects($vars);
+
+$title = $vars->exists('id') ? _("Edit Time") : _("New Time");
+require HERMES_TEMPLATES . '/common-header.inc';
+require HERMES_TEMPLATES . '/menu.inc';
+$form->renderActive(new Horde_Form_Renderer(), $vars, 'entry.php', 'post');
+require $registry->get('templates', 'horde') . '/common-footer.inc';
diff --git a/hermes/index.php b/hermes/index.php
new file mode 100644 (file)
index 0000000..55ba893
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+/**
+ * $Horde: hermes/index.php,v 1.16 2009/01/06 17:50:08 jan Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ */
+
+@define('HERMES_BASE', dirname(__FILE__));
+$hermes_configured = (is_readable(HERMES_BASE . '/config/conf.php'));
+
+if (!$hermes_configured) {
+    require HERMES_BASE . '/../lib/Test.php';
+    Horde_Test::configFilesMissing('Hermes', HERMES_BASE,
+        array('conf.php'));
+}
+
+require HERMES_BASE . '/time.php';
diff --git a/hermes/invoicing.php b/hermes/invoicing.php
new file mode 100644 (file)
index 0000000..2aab4ce
--- /dev/null
@@ -0,0 +1,136 @@
+<?php
+/**
+ * $Horde: hermes/invoicing.php,v 1.12 2009/06/10 17:33:20 slusarz Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ *
+ * @author Duck <duck@obala.net>
+ */
+
+@define('HERMES_BASE', dirname(__FILE__));
+require_once HERMES_BASE . '/lib/base.php';
+
+require_once 'Horde/Form.php';
+require_once 'Horde/Form/Renderer.php';
+require_once 'Horde/Form/Type/tableset.php';
+
+$hours = $hermes->getHours(array('billable' => true,
+                                 'submitted' => true));
+if (is_a($hours, 'PEAR_Error')) {
+    $notification->push($hours->getMessage(), 'horde.error');
+    header('Location: ' . Horde::applicationUrl('time.php'));
+    exit;
+} elseif (empty($hours)) {
+    $notification->push(_("There is no submitted billable hours."), 'horde.warning');
+    header('Location: ' . Horde::applicationUrl('time.php'));
+    exit;
+} elseif (!$registry->hasMethod('invoices/save')) {
+    $notification->push(_("Invoicing system is not installed."), 'horde.warning');
+    header('Location: ' . Horde::applicationUrl('time.php'));
+    exit;
+}
+
+$headers = array(
+    'client' => _("Client"),
+    'employee' => _("Employee"),
+    '_type_name' => _("Job Type"),
+    'rate' => _("Rate"),
+    'hours' => _("Hours"),
+    'total' => _("Total"),
+    'date' => _("Date"),
+    'description' => _("Description"),
+    'note' => _("Cost Object")
+);
+
+$clients = Hermes::listClients();
+$df = $GLOBALS['prefs']->getValue('date_format');
+
+$list = array();
+$client_keys = array();
+foreach ($hours as $hour) {
+    $id = (int)$hour['id'];
+    $client_keys[$id] = $hour['client'];
+    $list[$id] = array(
+        'client' => $clients[$hour['client']],
+        'employee' => $hour['employee'],
+        '_type_name' => $hour['_type_name'],
+        'rate' => $hour['rate'],
+        'hours' => $hour['hours'],
+        'total' => $hour['rate'] * $hour['hours'],
+        'date' => strftime($df, $hour['date']),
+        'description' => $hour['description'],
+        '_costobject_name' => $hour['_costobject_name'],
+    );
+}
+
+$title = _("Create invoice");
+$vars = Horde_Variables::getDefaultVariables();
+$form = new Horde_Form($vars, $title, 'create_invoice');
+
+$type_params = array(array(1 => _("Yes"), 0 => _("No")));
+$form->addVariable(_("Combine same clients in one invoice"), 'combine', 'enum', true, false, null, $type_params);
+$v = &$form->addVariable(_("Select hours to be invoiced"), 'hours', 'tableset', true, false, false, array($list, $headers));
+$v->setDefault(array_keys($list));
+
+if ($form->validate()) {
+    $form->getInfo(null, $info);
+
+    $groups = array();
+    if ($info['combine']) {
+        foreach ($info['hours'] as $id) {
+            $client = $client_keys[$id];
+            if (isset($groups[$client])) {
+                $groups[$client]['hours'][] = $id;
+            } else {
+                $groups[$client] = array('client' => $client,
+                                         'hours' => array($id));
+            }
+        }
+    } else {
+        foreach ($info['hours'] as $id) {
+            $groups[] = array('client' => $hours[$id]['client'],
+                              'hours' => array($id));
+        }
+    }
+
+    foreach ($groups as $group) {
+
+        $invoice = array();
+        $invoice['client'] = array('id' => $group['client']);
+        $invoice['invoice'] = array('type' =>    $conf['invoices']['params']['type'],
+                                    'status' =>  $conf['invoices']['params']['status'],
+                                    'expire' =>  $conf['invoices']['params']['expire'],
+                                    'place' =>   $conf['invoices']['params']['place'],
+                                    'service' => date('Y-m-d'));
+
+        $invoice['articles'] = array();
+        foreach ($group['hours'] as $hour) {
+            $invoice['articles'][] = array('name' => $list[$hour]['description'],
+                                           'price' => $list[$hour]['rate'],
+                                           'qt' => $list[$hour]['hours'],
+                                           'discount' => 0);
+        }
+
+        $invoice_id = $registry->call('invoices/save', array($invoice));
+        if (is_a($invoice_id, 'PEAR_Error')) {
+            $notification->push($invoice_id->getMessage(), 'horde.error');
+        } else {
+            $msg = sprintf(_("Invoice for client %s successfuly created."), $clients[$group['client']]);
+            $notification->push($msg, 'horde.success');
+        }
+    }
+
+    header('Location: ' . Horde::applicationUrl('time.php'));
+    exit;
+}
+
+require HERMES_TEMPLATES . '/common-header.inc';
+require HERMES_TEMPLATES . '/menu.inc';
+
+$renderer = new Horde_Form_Renderer(array('varrenderer_driver' => 'tableset_html'));
+$form->renderActive($renderer, null, Horde::applicationUrl('invoicing.php'), 'post');
+
+require $registry->get('templates', 'horde') . '/common-footer.inc';
diff --git a/hermes/lib/Admin.php b/hermes/lib/Admin.php
new file mode 100644 (file)
index 0000000..5ae46aa
--- /dev/null
@@ -0,0 +1,201 @@
+<?php
+/**
+ * $Horde: hermes/lib/Admin.php,v 1.26 2009/01/06 17:50:09 jan Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ *
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ */
+
+/**
+ * Horde_Form
+ */
+require_once 'Horde/Form.php';
+
+/**
+ * @package Hermes
+ */
+class AddJobTypeForm extends Horde_Form {
+
+    function AddJobTypeForm(&$vars)
+    {
+        parent::Horde_Form($vars, 'addjobtypeform');
+
+        $this->addVariable(_("Job Type"), 'name', 'text', true);
+        $var = &$this->addVariable(_("Enabled?"), 'enabled', 'boolean', false);
+        $var->setDefault(true);
+        $var = &$this->addVariable(_("Billable?"), 'billable', 'boolean', false);
+        $var->setDefault(true);
+        $this->addVariable(_("Hourly Rate"), 'rate', 'number', false);
+    }
+
+}
+
+/**
+ * @package Hermes
+ */
+class EditJobTypeStep1Form extends Horde_Form {
+
+    function EditJobTypeStep1Form(&$vars)
+    {
+        global $hermes;
+
+        parent::Horde_Form($vars, 'editjobtypestep1form');
+
+        $values = array();
+        $jobtypes = $hermes->listJobTypes();
+        if (!is_a($jobtypes, 'PEAR_Error')) {
+            foreach ($jobtypes as $id => $jobtype) {
+                $values[$id] = $jobtype['name'];
+                if (empty($jobtype['enabled'])) {
+                    $values[$id] .= _(" (DISABLED)");
+                }
+            }
+        }
+
+        if ($values) {
+            $subtype = 'enum';
+            $type_params = array($values);
+        } else {
+            $subtype = 'invalid';
+            $type_params = array(_("There are no job types to edit"));
+        }
+
+        $this->addVariable(_("JobType Name"), 'jobtype', $subtype, true, false, null, $type_params);
+    }
+
+}
+
+/**
+ * @package Hermes
+ */
+class EditJobTypeStep2Form extends Horde_Form {
+
+    function EditJobTypeStep2Form(&$vars)
+    {
+        global $hermes;
+
+        parent::Horde_Form($vars, 'editjobtypestep2form');
+
+        $jobtype = $vars->get('jobtype');
+        $info = $hermes->getJobTypeByID($jobtype);
+        if (!$info || is_a($info, 'PEAR_Error')) {
+            $stype = 'invalid';
+            $type_params = array(_("This is not a valid job type."));
+        } else {
+            $stype = 'text';
+            $type_params = array();
+        }
+
+        $this->addHidden('', 'jobtype', 'int', true, true);
+
+        $sname = &$this->addVariable(_("Job Type"), 'name', $stype, true, false, null, $type_params);
+        if (!empty($info['name'])) {
+            $sname->setDefault($info['name']);
+        }
+
+        $enab = &$this->addVariable(_("Enabled?"), 'enabled', 'boolean', false);
+        $enab->setDefault($info['enabled']);
+        $enab = &$this->addVariable(_("Billable?"), 'billable', 'boolean', false);
+        $enab->setDefault($info['billable']);
+        $enab = &$this->addVariable(_("Hourly Rate"), 'rate', 'number', false);
+        $enab->setDefault($info['rate']);
+    }
+
+}
+
+/**
+ * @package Hermes
+ */
+class DeleteJobTypeForm extends Horde_Form {
+
+    function DeleteJobTypeForm(&$vars)
+    {
+        global $hermes;
+
+        parent::Horde_Form($vars, 'deletejobtypeform');
+
+        $jobtype = $vars->get('jobtype');
+        $info = $hermes->getJobTypeByID($jobtype);
+
+        $yesnotype = 'enum';
+        $type_params = array(array(0 => _("No"), 1 => _("Yes")));
+
+        $this->addHidden('', 'jobtype', 'int', true, true);
+
+        $sname = &$this->addVariable(_("Job Type"), 'name', 'text', false, true);
+        $sname->setDefault($info['name']);
+
+        $this->addVariable(_("Really delete this job type? This may cause data problems!!"), 'yesno', $yesnotype, true, false, null, $type_params);
+    }
+
+}
+
+/**
+ * @package Hermes
+ */
+class EditClientStep1Form extends Horde_Form {
+
+    function EditClientStep1Form(&$vars)
+    {
+        global $hermes;
+
+        parent::Horde_Form($vars, 'editclientstep1form');
+
+        $clients = Hermes::listClients();
+        if (is_a($clients, 'PEAR_Error')) {
+            $subtype = 'invalid';
+            $type_params = array($clients->getMessage());
+        } elseif (count($clients)) {
+            $subtype = 'enum';
+            $type_params = array($clients);
+        } else {
+            $subtype = 'invalid';
+            $type_params = array(_("There are no clients to edit"));
+        }
+
+        $this->addVariable(_("Client Name"), 'client', $subtype, true, false, null, $type_params);
+    }
+
+}
+
+/**
+ * @package Hermes
+ */
+class EditClientStep2Form extends Horde_Form {
+
+    function EditClientStep2Form(&$vars)
+    {
+        global $hermes;
+
+        parent::Horde_Form($vars, 'editclientstep2form');
+
+        $client = $vars->get('client');
+        $info = $hermes->getClientSettings($client);
+        if (!$info || is_a($info, 'PEAR_Error')) {
+            $stype = 'invalid';
+            $type_params = array(_("This is not a valid client."));
+        } else {
+            $stype = 'text';
+            $type_params = array();
+        }
+
+        $this->addHidden('', 'client', 'text', true, true);
+        $name = &$this->addVariable(_("Client"), 'name', $stype, false, true, false, null, $type_params);
+        $name->setDefault($info['name']);
+
+        $enterdescription = &$this->addVariable(sprintf(_("Should users enter descriptions of their timeslices for this client? If not, the description will automatically be \"%s\"."), _("See Attached Timesheet")), 'enterdescription', 'boolean', true);
+        if (!empty($info['enterdescription'])) {
+            $enterdescription->setDefault($info['enterdescription']);
+        }
+
+        $exportid = &$this->addVariable(_("ID for this client when exporting data, if different from the name displayed above."), 'exportid', 'text', false);
+        if (!empty($info['exportid'])) {
+            $exportid->setDefault($info['exportid']);
+        }
+    }
+
+}
diff --git a/hermes/lib/Block/tree_menu.php b/hermes/lib/Block/tree_menu.php
new file mode 100644 (file)
index 0000000..ba90b71
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+
+$block_name = _("Enter Time");
+$block_type = 'tree';
+
+/**
+ * $Horde: hermes/lib/Block/tree_menu.php,v 1.4 2008/01/02 16:15:09 chuck Exp $
+ *
+ * @package Horde_Block
+ */
+class Horde_Block_hermes_tree_menu extends Horde_Block {
+
+    var $_app = 'hermes';
+
+    function _buildTree(&$tree, $indent = 0, $parent = null)
+    {
+        require_once dirname(__FILE__) . '/../base.php';
+
+        $tree->addNode($parent . '__add',
+                       $parent,
+                       _("Enter Time"),
+                       $indent + 1,
+                       false,
+                       array('icon' => 'hermes.png',
+                             'icondir' => $GLOBALS['registry']->getImageDir(),
+                             'url' => Horde::applicationUrl('entry.php')));
+        $tree->addNode($parent . '__search',
+                       $parent,
+                       _("Search Time"),
+                       $indent + 1,
+                       false,
+                       array('icon' => 'search.png',
+                             'icondir' => $GLOBALS['registry']->getImageDir('horde'),
+                             'url' => Horde::applicationUrl('search.php')));
+    }
+
+}
diff --git a/hermes/lib/Block/tree_stopwatch.php b/hermes/lib/Block/tree_stopwatch.php
new file mode 100644 (file)
index 0000000..ef83369
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+
+$block_name = _("Stop Watch");
+$block_type = 'tree';
+
+/**
+ * $Horde: hermes/lib/Block/tree_stopwatch.php,v 1.4 2009/06/10 05:24:07 slusarz Exp $
+ *
+ * @package Horde_Block
+ */
+class Horde_Block_hermes_tree_stopwatch extends Horde_Block {
+
+    var $_app = 'hermes';
+
+    function _buildTree(&$tree, $indent = 0, $parent = null)
+    {
+        global $registry, $prefs;
+
+        require_once dirname(__FILE__) . '/../base.php';
+
+        Horde::addScriptFile('popup.js', 'horde', true);
+
+        $entry = Horde::applicationUrl('entry.php');
+        $icondir = $registry->getImageDir();
+
+        $tree->addNode($parent . '__start',
+                       $parent,
+                       _("Start Watch"),
+                       $indent + 1,
+                       false,
+                       array('icon' => 'timer-start.png',
+                             'icondir' => $icondir,
+                             'url' => '#',
+                             'onclick' => "popup('" . Horde::applicationUrl('start.php') . "', 400, 100); return false;"));
+
+        $timers = @unserialize($prefs->getValue('running_timers', false));
+        if ($timers) {
+            foreach ($timers as $i => $timer) {
+                $hours = round((float)(time() - $i) / 3600, 2);
+                $tname = Horde_String::convertCharset($timer['name'], $prefs->getCharset()) . sprintf(" (%s)", $hours);
+                $tree->addNode($parent . '__timer_' . $i,
+                               $parent,
+                               $tname,
+                               $indent + 1,
+                               false,
+                               array('icon' => 'timer-stop.png',
+                                     'icondir' => $icondir,
+                                     'url' => Horde_Util::addParameter($entry, 'timer', $i),
+                                     'target' => 'horde_main'));
+            }
+        }
+    }
+
+}
diff --git a/hermes/lib/Data/hermes_csv.php b/hermes/lib/Data/hermes_csv.php
new file mode 100644 (file)
index 0000000..e7ed61c
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+
+require_once 'Horde/Data/csv.php';
+
+/**
+ * The Horde_Data_hermes_csv class extends Horde's CSV Data class with
+ * Hermes-specific handling.
+ *
+ * $Horde: hermes/lib/Data/hermes_csv.php,v 1.2 2005/08/03 20:54:02 chuck Exp $
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ *
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ * @package Horde_Data
+ */
+class Horde_Data_hermes_csv extends Horde_Data_csv {
+
+    var $_mapped = false;
+
+    function exportData($data)
+    {
+        return parent::exportData($this->_map($data), true);
+    }
+
+    function _map($data)
+    {
+        if ($this->_mapped) {
+            return $data;
+        }
+
+        $this->_mapped = true;
+
+        $count = count($data);
+        for ($i = 0; $i < $count; $i++) {
+            $data[$i]['description'] = str_replace(array("\r", "\n"), array('', ' '), $data[$i]['description']);
+            $data[$i]['note'] = str_replace(array("\r", "\n"), array('', ' '), $data[$i]['note']);
+            $data[$i]['timestamp'] = $data[$i]['date'];
+            $data[$i]['date'] = date('m/d/y', $data[$i]['date']);
+            $data[$i]['duration'] = date('H:i', mktime(0, $data[$i]['hours'] * 60));
+            $data[$i]['billable'] = $data[$i]['billable'] == 2 ? '' : $data[$i]['billable'];
+        }
+
+        return $data;
+    }
+
+}
diff --git a/hermes/lib/Data/hermes_tsv.php b/hermes/lib/Data/hermes_tsv.php
new file mode 100644 (file)
index 0000000..ef4bded
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+
+require_once 'Horde/Data/tsv.php';
+
+/**
+ * The Horde_Data_hermes_tsv class extends Horde's TSV Data class with
+ * Hermes-specific handling.
+ *
+ * $Horde: hermes/lib/Data/hermes_tsv.php,v 1.2 2005/08/03 20:54:02 chuck Exp $
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ *
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ * @package Horde_Data
+ */
+class Horde_Data_hermes_tsv extends Horde_Data_tsv {
+
+    var $_mapped = false;
+
+    function exportData($data)
+    {
+        return parent::exportData($this->_map($data), true);
+    }
+
+    function _map($data)
+    {
+        if ($this->_mapped) {
+            return $data;
+        }
+
+        $this->_mapped = true;
+
+        $count = count($data);
+        for ($i = 0; $i < $count; $i++) {
+            $data[$i]['description'] = str_replace(array("\r", "\n"), array('', ' '), $data[$i]['description']);
+            $data[$i]['note'] = str_replace(array("\r", "\n"), array('', ' '), $data[$i]['note']);
+            $data[$i]['timestamp'] = $data[$i]['date'];
+            $data[$i]['date'] = date('m/d/y', $data[$i]['date']);
+            $data[$i]['duration'] = date('H:i', mktime(0, $data[$i]['hours'] * 60));
+            $data[$i]['billable'] = $data[$i]['billable'] == 2 ? '' : $data[$i]['billable'];
+        }
+
+        return $data;
+    }
+
+}
diff --git a/hermes/lib/Data/hermes_xls.php b/hermes/lib/Data/hermes_xls.php
new file mode 100644 (file)
index 0000000..97c660c
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+
+require_once HERMES_BASE . '/lib/Data/hermes_tsv.php';
+
+/**
+ * The Horde_Data_hermes_xls class extends Horde's TSV Data class with
+ * Hermes-specific handling and a few tweaks for files to open
+ * directly in Excel.
+ *
+ * $Horde: hermes/lib/Data/hermes_xls.php,v 1.2 2005/08/03 20:54:02 chuck Exp $
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ *
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ * @package Horde_Data
+ */
+class Horde_Data_hermes_xls extends Horde_Data_hermes_tsv {
+
+    var $_extension = 'xls';
+    var $_contentType = 'application/msexcel';
+
+}
diff --git a/hermes/lib/Data/iif.php b/hermes/lib/Data/iif.php
new file mode 100644 (file)
index 0000000..da97fab
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+/**
+ * The Horde_Data_iif class implements the Horde_Data:: framework for
+ * QuickBooks .iif files.
+ *
+ * $Horde: hermes/lib/Data/iif.php,v 1.21 2009/01/06 17:50:09 jan Exp $
+ *
+ * Here's a sample header and row from a .iif file (it's
+ * tab-delimited):
+ *
+ * !TIMEACT        DATE        JOB        EMP        ITEM        DURATION        NOTE        BILLINGSTATUS        PITEM
+ * TIMEACT        07/30/02        A Company:Their Projec        Sylvester Employee        Programming        10:00                1        Not Applicable
+ *
+ * $Horde: hermes/lib/Data/iif.php,v 1.21 2009/01/06 17:50:09 jan Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ *
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ * @package Horde_Data
+ */
+class Horde_Data_iif extends Horde_Data {
+
+    var $_extension = 'iif';
+    var $_contentType = 'text/plain';
+    var $_rawData;
+    var $_iifData;
+    var $_mapped = false;
+
+    function exportData($data)
+    {
+        $this->_rawData = $data;
+        $newline = $this->getNewline();
+
+        $this->_map();
+
+        $data = "!TIMEACT\tDATE\tJOB\tEMP\tITEM\tDURATION\tNOTE\tBILLINGSTATUS\tPITEM$newline";
+        foreach ($this->_iifData as $row) {
+            $data .= implode("\t", $row) . $newline;
+        }
+
+        return $data;
+    }
+
+    function _map()
+    {
+        if ($this->_mapped) {
+            return;
+        }
+
+        $this->_mapped = true;
+
+        foreach ($this->_rawData as $row) {
+            $row['description'] = str_replace(array("\r", "\n"), array('', ' '), $row['description']);
+            $row['note'] = str_replace(array("\r", "\n"), array('', ' '), $row['note']);
+            $this->_iifData[] = array('_label' => 'TIMEACT',
+                                      'DATE' => date('m/d/y', $row['date']),
+                                      'JOB' => $row['client'],
+                                      'EMP' => $row['employee'],
+                                      'ITEM' => $row['item'],
+                                      'DURATION' => date('H:i', mktime(0, $row['hours'] * 60)),
+                                      'NOTE' => $row['description'] . (!empty($row['note']) ? _("; Notes: ") . $row['note'] : ''),
+                                      'BILLINGSTATUS' => $row['billable'] == 2 ? '' : $row['billable'],
+                                      'PITEM' => 'Not Applicable');
+        }
+    }
+
+}
diff --git a/hermes/lib/Data/qbxml.php b/hermes/lib/Data/qbxml.php
new file mode 100644 (file)
index 0000000..851dca0
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+/**
+ * The Horde_Data_qbxml class implements the Horde_Data:: framework for
+ * QuickBooks qbXML files.
+ *
+ * $Horde: hermes/lib/Data/qbxml.php,v 1.15 2009/01/06 17:50:09 jan Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ *
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ * @package Horde_Data
+ */
+class Horde_Data_qbxml extends Horde_Data {
+
+    var $_extension = 'xml';
+
+}
diff --git a/hermes/lib/Driver.php b/hermes/lib/Driver.php
new file mode 100644 (file)
index 0000000..868dbdd
--- /dev/null
@@ -0,0 +1,225 @@
+<?php
+/**
+ * Hermes_Driver:: defines an API for implementing storage backends
+ * for Hermes.
+ *
+ * $Horde: hermes/lib/Driver.php,v 1.23 2009/07/14 00:25:33 mrubinsk Exp $
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @since   Hermes 0.1
+ * @package Hermes
+ */
+class Hermes_Driver {
+
+    /**
+     * Retrieve a specific job type record.
+     *
+     * @param integer $jobTypeID            The ID of the job type.
+     *
+     * @return mixed Hash of job type properties, or PEAR_Error on failure.
+     */
+    function getJobTypeByID($jobTypeID)
+    {
+        $jobtypes = $this->listJobTypes(array('id' => $jobTypeID));
+        if (is_a($jobtypes, 'PEAR_Error')) {
+            return $jobtypes;
+        }
+        if (!isset($jobtypes[$jobTypeID])) {
+            return PEAR::raiseError(sprintf(_("No job type with ID \"%s\"."),
+                                            $jobTypeID));
+        }
+        return $jobtypes[$jobTypeID];
+    }
+
+    /**
+     * Add or update a job type record.
+     *
+     * @abstract
+     * @param array $jobtype        A hash of job type properties:
+     *                  'id'        => The ID of the job, if updating.  If not
+     *                                 present, a new job type is created.
+     *                  'name'      => The job type's name.
+     *                  'enabled'   => Whether the job type is enabled for new
+     *                                 time entry.
+     *
+     * @return mixed The job's ID, or PEAR_Error on failure.
+     */
+    function updateJobType($jobtype)
+    {
+        return PEAR::raiseError(_("Not implemented."));
+    }
+
+    /**
+     * Retrieve list of job types.
+     *
+     * @abstract
+     *
+     * @param array $criteria  Hash of filter criteria:
+     *
+     *                      'enabled' => If present, only retrieve enabled
+     *                                   or disabled job types.
+     *
+     * @return mixed Associative array of job types, or PEAR_Error on failure.
+     */
+    function listJobTypes($criteria = array())
+    {
+        return PEAR::raiseError(_("Not implemented."));
+    }
+
+    /**
+     * Retrieve a deliverable by ID.
+     *
+     * @param integer $deliverableID        The ID of the deliverable to
+     *                                      retrieve.
+     * @return mixed Hash of deliverable's properties, or PEAR_Error on
+     *               failure.
+     */
+    function getDeliverableByID($deliverableID)
+    {
+        $deliverables = $this->listDeliverables(array('id' => $deliverableID));
+        if (is_a($deliverables, 'PEAR_Error')) {
+            return $deliverables;
+        }
+
+        if (!isset($deliverables[$deliverableID])) {
+            return PEAR::raiseError(sprintf(_("Deliverable %d not found."),
+                                            $deliverableID));
+        }
+
+        return $deliverables[$deliverableID];
+    }
+
+    /**
+     * Add or update a deliverable.
+     *
+     * @abstract
+     * @param array $deliverable    A hash of deliverable properties:
+     *                  'id'            => The ID of the deliverable, if
+     *                                     updating.  If not present, a new
+     *                                     ID is allocated.
+     *                  'name'          => The deliverable's display name.
+     *                  'client_id'     => The assigned client ID.
+     *                  'parent'        => ID of the deliverables parent
+     *                                     deliverable (if a child).
+     *                  'estimate'      => Estimated number of hours for
+     *                                     completion of the deliverable.
+     *                  'active'        => Whether this deliverable is active.
+     *                  'description'   => Text description (notes) for this
+     *                                     deliverable.
+     *
+     * @return mixed Integer ID of new or saved deliverable, or PEAR_Error on
+     *               failure.
+     */
+    function updateDeliverable($deliverable)
+    {
+        return PEAR::raiseError(_("Not implemented."));
+    }
+
+    /**
+     * Retrieve list of deliverables.
+     *
+     * @abstract
+     *
+     * @param array $criteria  A hash of search criteria:
+     *              'id'        => If present, only deliverable with
+     *                             specified ID is searched for.
+     *              'client_id' => If present, list is filtered by
+     *                             client ID.
+     *
+     * @return mixed Associative array of job types, or PEAR_Error on failure.
+     */
+    function listDeliverables($criteria = array())
+    {
+        return PEAR::raiseError(_("Not implemented."));
+    }
+
+    /**
+     * Delete a deliverable.
+     *
+     * @abstract
+     * @param integer $deliverableID        The ID of the deliverable.
+     *
+     * @return mixed Null, or PEAR_Error on failure.
+     */
+    function deleteDeliverable($deliverableID)
+    {
+        return PEAR::raiseError(_("Not implemented."));
+    }
+
+    /**
+     * Attempts to return a concrete Hermes_Driver instance based on $driver.
+     *
+     * @param string $driver  The type of concrete Hermes_Driver subclass to
+     *                        return.
+     * @param array $params   A hash containing any additional configuration or
+     *                        connection parameters a subclass might need.
+     *
+     * @return mixed  The newly created concrete Hermes_Driver instance, or
+     *                false on error.
+     */
+    function &factory($driver = null, $params = null)
+    {
+        if (is_null($driver)) {
+            $driver = $GLOBALS['conf']['storage']['driver'];
+        }
+
+        $driver = basename($driver);
+
+        if (is_null($params)) {
+            $params = Horde::getDriverConfig('storage', $driver);
+        }
+
+        $class = 'Hermes_Driver_' . $driver;
+        if (class_exists($class)) {
+            $hermes = new $class($params);
+        } else {
+            $hermes = false;
+        }
+
+        return $hermes;
+    }
+
+    /**
+     * Attempts to return a reference to a concrete Hermes_Driver instance
+     * based on $driver.
+     *
+     * It will only create a new instance if no Hermes_Driver instance with the
+     * same parameters currently exists.
+     *
+     * This should be used if multiple storage sources are required.
+     *
+     * This method must be invoked as: $var = &Hermes_Driver::singleton()
+     *
+     * @param string $driver  The type of concrete Hermes_Driver subclass to
+     *                        return.
+     * @param array $params   A hash containing any additional configuration or
+     *                        connection parameters a subclass might need.
+     *
+     * @return mixed  The created concrete Hermes_Driver instance, or false on
+     *                error.
+     */
+    function &singleton($driver = null, $params = null)
+    {
+        static $instances;
+
+        if (is_null($driver)) {
+            $driver = $GLOBALS['conf']['storage']['driver'];
+        }
+
+        if (is_null($params)) {
+            $params = Horde::getDriverConfig('storage', $driver);
+        }
+
+        if (!isset($instances)) {
+            $instances = array();
+        }
+
+        $signature = serialize(array($driver, $params));
+        if (!isset($instances[$signature])) {
+            $instances[$signature] = &Hermes_Driver::factory($driver, $params);
+        }
+
+        return $instances[$signature];
+    }
+
+}
diff --git a/hermes/lib/Driver/sql.php b/hermes/lib/Driver/sql.php
new file mode 100644 (file)
index 0000000..433e2c9
--- /dev/null
@@ -0,0 +1,748 @@
+<?php
+/**
+ * Hermes storage implementation for PHP's PEAR database abstraction layer.
+ *
+ * $Horde: hermes/lib/Driver/sql.php,v 1.68 2009/07/14 00:25:33 mrubinsk Exp $
+ *
+ * Required values for $params:<pre>
+ *      'phptype'       The database type (e.g. 'pgsql', 'mysql', etc.).</pre>
+ *
+ * Required by some database implementations:<pre>
+ *      'hostspec'      The hostname of the database server.
+ *      'protocol'      The communication protocol ('tcp', 'unix', etc.).
+ *      'database'      The name of the database.
+ *      'username'      The username with which to connect to the database.
+ *      'password'      The password associated with 'username'.
+ *      'options'       Additional options to pass to the database.
+ *      'tty'           The TTY on which to connect to the database.
+ *      'port'          The port on which to connect to the database.</pre>
+ *
+ * The table structure can be created by the
+ * scripts/drivers/hermes.sql script.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org
+ * @package Hermes
+ */
+class Hermes_Driver_sql extends Hermes_Driver {
+
+    /**
+     * Hash containing connection parameters.
+     *
+     * @var array
+     */
+    var $_params = array();
+
+    /**
+     * Handle for the current database connection.
+     *
+     * @var DB
+     */
+    var $_db;
+
+    /**
+     * Boolean indicating whether or not we're connected to the SQL server.
+     *
+     * @var boolean
+     */
+    var $_connected = false;
+
+    /**
+     * Constructs a new SQL storage object.
+     *
+     * @param string $user      The user who owns these billing.
+     * @param array  $params    A hash containing connection parameters.
+     */
+    function Hermes_Driver_sql($params = array())
+    {
+        $this->_params = $params;
+    }
+
+    /**
+     * Save a row of billing information.
+     *
+     * @param string $employee  The Horde ID of the person who worked the
+     *                          hours.
+     * @param array $entries    The billing information to enter. Each array
+     *                          row must contain the following entries:
+     *             'date'         The day the hours were worked (ISO format)
+     *             'client'       The id of the client the work was done for.
+     *             'type'         The type of work done.
+     *             'hours'        The number of hours worked
+     *             'rate'         The hourly rate the work was done at.
+     *             'billable'     (optional) Whether or not the work is
+     *                            billable hours.
+     *             'description'  A short description of the work.
+     *
+     * @return mixed  True on success, PEAR_Error on failure.
+     */
+    function enterTime($employee, $info)
+    {
+        require_once 'Date.php';
+
+        /* Make sure we have a valid database connection. */
+        $this->_connect();
+
+        /* Get job rate */
+        $sql = 'SELECT jobtype_rate FROM hermes_jobtypes WHERE jobtype_id = ?';
+        $job_rate = $this->_db->getOne($sql, array($info['type']));
+
+        $dt = new Date($info['date']);
+        $timeslice_id = $this->_db->nextId('hermes_timeslices');
+        $sql = 'INSERT INTO hermes_timeslices (timeslice_id, ' .
+               'clientjob_id, employee_id, jobtype_id, ' .
+               'timeslice_hours, timeslice_isbillable, ' .
+               'timeslice_date, timeslice_description, ' .
+               'timeslice_note, timeslice_rate, costobject_id) ' .
+               'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
+        $values = array((int)$timeslice_id,
+                        $info['client'],
+                        $employee,
+                        $info['type'],
+                        $info['hours'],
+                        isset($info['billable']) ? (int)$info['billable'] : 0,
+                        (int)$dt->getTime(DATE_FORMAT_UNIXTIME) + 1,
+                        $info['description'],
+                        $info['note'],
+                        (float)$job_rate,
+                        (empty($info['costobject']) ? null :
+                         $info['costobject']));
+
+        Horde::logMessage($sql, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        return $this->_db->query($sql, $values);
+    }
+
+    /**
+     * Update a set of billing information.
+     *
+     * @param array $entries  The billing information to enter. Each array row
+     *                        must contain the following entries:
+     *              'id'           The id of this time entry.
+     *              'date'         The day the hours were worked (ISO format)
+     *              'client'       The id of the client the work was done for.
+     *              'type'         The type of work done.
+     *              'hours'        The number of hours worked
+     *              'rate'         The hourly rate the work was done at.
+     *              'billable'     Whether or not the work is billable hours.
+     *              'description'  A short description of the work.
+     *
+     *                        If any rows contain a 'delete' entry, those rows
+     *                        will be deleted instead of updated.
+     *
+     * @return mixed  True on success, PEAR_Error on failure.
+     */
+    function updateTime($entries)
+    {
+        require_once 'Date.php';
+
+        /* Make sure we have a valid database connection. */
+        $this->_connect();
+
+        foreach ($entries as $info) {
+            if (!Hermes::canEditTimeslice($info['id'])) {
+                return PEAR::raiseError(_("Access denied; user cannot modify this timeslice."));
+            }
+            if (!empty($info['delete'])) {
+                $sql = 'DELETE FROM hermes_timeslices WHERE timeslice_id = ?';
+                $values = array((int)$info['id']);
+            } else {
+                if (isset($info['employee'])) {
+                    $employee_cl = ' employee_id = ?,';
+
+                    $values = array($info['employee']);
+                } else {
+                    $employee_cl = '';
+                }
+                $dt = new Date($info['date']);
+                $sql = 'UPDATE hermes_timeslices SET' . $employee_cl .
+                       ' clientjob_id = ?, jobtype_id = ?,' .
+                       ' timeslice_hours = ?, timeslice_isbillable = ?,' .
+                       ' timeslice_date = ?, timeslice_description = ?,' .
+                       ' timeslice_note = ?, costobject_id = ?' .
+                       ' WHERE timeslice_id = ?';
+                $values = array($info['client'],
+                                $info['type'],
+                                $info['hours'],
+                                (isset($info['billable']) ? (int)$info['billable'] : 0),
+                                (int)$dt->getTime(DATE_FORMAT_UNIXTIME) + 1,
+                                $info['description'],
+                                $info['note'],
+                                (empty($info['costobject']) ? null : $info['costobject']),
+                                (int)$info['id']);
+            }
+
+            Horde::logMessage($sql, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+            $result = $this->_db->query($sql, $values);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        return true;
+    }
+
+    function getHours($filters = array(), $fields = array())
+    {
+        global $conf;
+
+        /* Make sure we have a valid database connection. */
+        $this->_connect();
+
+        $fieldlist = array(
+            'id' => 'b.timeslice_id as id',
+            'client' => ' b.clientjob_id as client',
+            'employee' => ' b.employee_id as employee',
+            'type' => ' b.jobtype_id as type',
+            '_type_name' => ' j.jobtype_name as "_type_name"',
+            'hours' => ' b.timeslice_hours as hours',
+            'rate' => ' b.timeslice_rate as rate',
+            'billable' => empty($conf['time']['choose_ifbillable'])
+                ? ' j.jobtype_billable as billable'
+                : ' b.timeslice_isbillable as billable',
+            'date' => ' b.timeslice_date as "date"',
+            'description' => ' b.timeslice_description as description',
+            'note' => ' b.timeslice_note as note',
+            'submitted' => ' b.timeslice_submitted as submitted',
+            'costobject' => ' b.costobject_id as costobject');
+        if (!empty($fields)) {
+            $fieldlist = array_keys(array_intersect(array_flip($fieldlist), $fields));
+        }
+        $fieldlist = implode(', ', $fieldlist);
+        $sql = 'SELECT ' . $fieldlist . ' FROM hermes_timeslices b INNER JOIN hermes_jobtypes j ON b.jobtype_id = j.jobtype_id';
+        if (count($filters) > 0) {
+            $where = '';
+            $glue = '';
+            foreach ($filters as $field => $filter) {
+                switch ($field) {
+                case 'client':
+                    $where .= $glue . $this->_equalClause('b.clientjob_id',
+                                                          $filter);
+                    $glue = ' AND';
+                    break;
+
+                case 'jobtype':
+                    $where .= $glue . $this->_equalClause('b.jobtype_id',
+                                                          $filter);
+                    $glue = ' AND';
+                    break;
+
+                case 'submitted':
+                    $where .= $glue . ' timeslice_submitted = ' . (int)$filter;
+                    $glue = ' AND';
+                    break;
+
+                case 'exported':
+                    $where .= $glue . ' timeslice_exported = ' . (int)$filter;
+                    $glue = ' AND';
+                    break;
+
+                case 'billable':
+                    $where .= $glue
+                        . (empty($conf['time']['choose_ifbillable'])
+                           ? ' jobtype_billable = '
+                           : ' timeslice_isbillable = ')
+                        . (int)$filter;
+                    $glue = ' AND';
+                    break;
+
+                case 'start':
+                    $where .= $glue . ' timeslice_date >= ' . (int)$filter;
+                    $glue = ' AND';
+                    break;
+
+                case 'end':
+                    $where .= $glue . ' timeslice_date <= ' . (int)$filter;
+                    $glue = ' AND';
+                    break;
+
+                case 'employee':
+                    $where .= $glue . $this->_equalClause('employee_id',
+                                                          $filter);
+                    $glue = ' AND';
+                    break;
+
+                case 'id':
+                    $where .= $glue . $this->_equalClause('timeslice_id',
+                                                          (int)$filter, false);
+                    $glue = ' AND';
+                    break;
+
+                case 'costobject':
+                    $where .= $glue . $this->_equalClause('costobject_id',
+                                                          $filter);
+                    $glue = ' AND';
+                    break;
+                }
+            }
+        if (!empty($where)) {
+            $sql .= ' WHERE ' . $where;
+        }
+        }
+
+        $sql .= ' ORDER BY timeslice_date DESC, clientjob_id';
+
+        Horde::logMessage($sql, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $hours = $this->_db->getAll($sql, DB_FETCHMODE_ASSOC);
+        if (is_a($hours, 'PEAR_Error')) {
+            return $hours;
+        }
+
+        // Add cost object names to the results.
+        if (empty($fields) || in_array('costobject', $fields)) {
+            foreach (array_keys($hours) as $hkey) {
+                if (empty($hours[$hkey]['costobject'])) {
+                    $hours[$hkey]['_costobject_name'] = '';
+                } else {
+                    $costobject = Hermes::getCostObjectByID($hours[$hkey]['costobject']);
+                    if (is_a($costobject, 'PEAR_Error')) {
+                        $hours[$hkey]['_costobject_name'] = sprintf(_("Error: %s"), $costobject->getMessage());
+                    } else {
+                        $hours[$hkey]['_costobject_name'] = $costobject['name'];
+                    }
+                }
+            }
+        }
+
+        return $hours;
+    }
+
+    /**
+     * @access private
+     */
+    function _equalClause($lhs, $rhs, $quote = true)
+    {
+        require_once 'Horde/SQL.php';
+
+        if (!is_array($rhs)) {
+            if ($quote) {
+                return sprintf(' %s = %s', $lhs, $this->_db->quote($rhs));
+            }
+            return sprintf(' %s = %s', $lhs, $rhs);
+        }
+
+        if (count($rhs) == 0) {
+            return ' FALSE';
+        }
+
+        $glue = '';
+        $ret = sprintf(' %s IN ( ', $lhs);
+        foreach ($rhs as $value) {
+            $ret .= $glue . $this->_db->quote($value);
+            $glue = ', ';
+        }
+        return $ret . ' )';
+    }
+
+    function markAs($field, $hours)
+    {
+        if (!count($hours)) {
+            return false;
+        }
+
+        $this->_connect();
+
+        switch ($field) {
+        case 'submitted':
+            $h_field = 'timeslice_submitted';
+            break;
+
+        case 'exported':
+            $h_field = 'timeslice_exported';
+            break;
+
+        default:
+            return false;
+        }
+
+        $ids = array();
+        foreach ($hours as $entry) {
+            $ids[] = (int)$entry['id'];
+        }
+
+        $sql = 'UPDATE hermes_timeslices SET ' . $h_field . ' = 1' .
+               ' WHERE timeslice_id IN (' . implode(',', $ids) . ')';
+
+        Horde::logMessage($sql, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        return $this->_db->query($sql);
+    }
+
+    function listJobTypes($criteria = array())
+    {
+        /* Make sure we have a valid database connection. */
+        $this->_connect();
+
+        $where = array();
+        $values = array();
+        if (isset($criteria['id'])) {
+            $where[] = 'jobtype_id = ?';
+            $values[] = $criteria['id'];
+        }
+        if (isset($criteria['enabled'])) {
+            $where[] = 'jobtype_enabled = ?';
+            $values[] = ($criteria['enabled'] ? 1 : 0);
+        }
+
+        $sql = 'SELECT jobtype_id, jobtype_name, jobtype_enabled' .
+               ', jobtype_rate, jobtype_billable FROM hermes_jobtypes' .
+               (empty($where) ? '' : (' WHERE ' . join(' AND ', $where))) .
+               ' ORDER BY jobtype_name';
+
+        Horde::logMessage($sql, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        $result = $this->_db->query($sql, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $results = array();
+        $row = $result->fetchRow(DB_FETCHMODE_ASSOC);
+        while (!empty($row) && !is_a($row, 'PEAR_Error')) {
+            $id = $row['jobtype_id'];
+            $results[$id] = array('id'       => $id,
+                                  'name'     => $row['jobtype_name'],
+                                  'rate'     => (float)$row['jobtype_rate'],
+                                  'billable' => (int)$row['jobtype_billable'],
+                                  'enabled'  => !empty($row['jobtype_enabled']));
+            $row = $result->fetchRow(DB_FETCHMODE_ASSOC);
+        }
+        if (is_a($row, 'PEAR_Error')) {
+            return $row;
+        }
+
+        return $results;
+    }
+
+    function updateJobType($jobtype)
+    {
+        $this->_connect();
+
+        if (!isset($jobtype['enabled'])) {
+            $jobtype['enabled'] = 1;
+        }
+        if (!isset($jobtype['billable'])) {
+            $jobtype['billable'] = 1;
+        }
+        if (empty($jobtype['id'])) {
+            $jobtype['id'] = $this->_db->nextId('hermes_jobtypes');
+            $sql = 'INSERT INTO hermes_jobtypes (jobtype_id, jobtype_name, ' .
+                   ' jobtype_enabled, jobtype_rate, jobtype_billable) VALUES (?, ?, ?, ?, ?)';
+            $values = array($jobtype['id'], $jobtype['name'],
+                            (int)$jobtype['enabled'], (float)$jobtype['rate'], (int)$jobtype['billable']);
+        } else {
+            $sql = 'UPDATE hermes_jobtypes' .
+                   ' SET jobtype_name = ?, jobtype_enabled = ?, jobtype_rate = ?,' .
+                   ' jobtype_billable = ?  WHERE jobtype_id = ?';
+            $values = array($jobtype['name'], (int)$jobtype['enabled'],(float)$jobtype['rate'],
+                            (int)$jobtype['billable'], $jobtype['id']);
+        }
+
+        Horde::logMessage($sql, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        $result = $this->_db->query($sql, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        return $jobtype['id'];
+    }
+
+    function deleteJobType($jobTypeID)
+    {
+        /* Make sure we have a valid database connection. */
+        $this->_connect();
+
+        $sql = 'DELETE FROM hermes_jobtypes WHERE jobtype_id = ?';
+        $values = array($jobTypeID);
+
+        Horde::logMessage($sql, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        return $this->_db->query($sql, $values);
+    }
+
+    function updateDeliverable($deliverable)
+    {
+        $this->_connect();
+
+        if (empty($deliverable['id'])) {
+            $deliverable['id'] = $this->_db->nextId('hermes_deliverables');
+            $sql = 'INSERT INTO hermes_deliverables (deliverable_id,' .
+                   ' client_id, deliverable_name, deliverable_parent,' .
+                   ' deliverable_estimate, deliverable_active,' .
+                   ' deliverable_description) VALUES (?, ?, ?, ?, ?, ?, ?)';
+            $values = array((int)$deliverable['id'],
+                            $deliverable['client_id'],
+                            $deliverable['name'],
+                            (empty($deliverable['parent']) ? null :
+                             (int)$deliverable['parent']),
+                            (empty($deliverable['estimate']) ? null :
+                             $deliverable['estimate']),
+                            ($deliverable['active'] ? 1 : 0),
+                            (empty($deliverable['description']) ? null :
+                             $deliverable['description']));
+        } else {
+            $sql = 'UPDATE hermes_deliverables SET client_id = ?,' .
+                   ' deliverable_name = ?, deliverable_parent = ?,' .
+                   ' deliverable_estimate = ?, deliverable_active = ?,' .
+                   ' deliverable_description = ? WHERE deliverable_id = ?';
+            $values = array($deliverable['client_id'],
+                            $deliverable['name'],
+                            (empty($deliverable['parent']) ? null :
+                             (int)$deliverable['parent']),
+                            (empty($deliverable['estimate']) ? null :
+                             $deliverable['estimate']),
+                            ($deliverable['active'] ? 1 : 0),
+                            (empty($deliverable['description']) ? null :
+                             $deliverable['description']),
+                            $deliverable['id']);
+        }
+
+        Horde::logMessage($sql, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        $result = $this->_db->query($sql, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        return $deliverable['id'];
+    }
+
+    function listDeliverables($criteria = array())
+    {
+        $this->_connect();
+
+        $where = array();
+        $values = array();
+        if (isset($criteria['id'])) {
+            $where[] = 'deliverable_id = ?';
+            $values[] = $criteria['id'];
+        }
+        if (isset($criteria['client_id'])) {
+            $where[] = 'client_id = ?';
+            $values[] = $criteria['client_id'];
+        }
+        if (isset($criteria['active'])) {
+            if ($criteria['active']) {
+                $where[] = 'deliverable_active <> ?';
+            } else {
+                $where[] = 'deliverable_active = ?';
+            }
+            $values[] = 0;
+        }
+
+        $sql = 'SELECT * FROM hermes_deliverables' .
+               (count($where) ? ' WHERE ' . join(' AND ', $where) : '');
+
+        Horde::logMessage($sql, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        $result = $this->_db->query($sql, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $deliverables = array();
+        $row = $result->fetchRow(DB_FETCHMODE_ASSOC);
+        while (!empty($row) && !is_a($row, 'PEAR_Error')) {
+
+            $deliverable = array('id'          => $row['deliverable_id'],
+                                 'client_id'   => $row['client_id'],
+                                 'name'        => $row['deliverable_name'],
+                                 'parent'      => $row['deliverable_parent'],
+                                 'estimate'    => $row['deliverable_estimate'],
+                                 'active'      => !empty($row['deliverable_active']),
+                                 'description' => $row['deliverable_description']);
+            $deliverables[$row['deliverable_id']] = $deliverable;
+            $row = $result->fetchRow(DB_FETCHMODE_ASSOC);
+        }
+
+        if (is_a($row, 'PEAR_Error')) {
+            return $row;
+        }
+
+        return $deliverables;
+    }
+
+    function deleteDeliverable($deliverableID)
+    {
+        $this->_connect();
+
+        $sql = 'SELECT COUNT(*) AS c FROM hermes_deliverables' .
+               ' WHERE deliverable_parent = ?';
+        $values = array($deliverableID);
+
+        Horde::logMessage($sql, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        $result = $this->_db->query($sql, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+        $row = $result->fetchRow(DB_FETCHMODE_ASSOC);
+        if (!empty($row['c'])) {
+            return PEAR::raiseError(_("Cannot delete deliverable; it has children."));
+        }
+
+        $sql = 'SELECT COUNT(*) AS c FROM hermes_timeslices' .
+               ' WHERE costobject_id = ?';
+        $values = array($deliverableID);
+
+        Horde::logMessage($sql, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        $result = $this->_db->query($sql, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+        $row = $result->fetchRow(DB_FETCHMODE_ASSOC);
+        if (!empty($row['c'])) {
+            return PEAR::raiseError(_("Cannot delete deliverable; there is time entered on it."));
+        }
+
+        $sql = 'DELETE FROM hermes_deliverables WHERE deliverable_id = ?';
+        $values = array($deliverableID);
+
+        Horde::logMessage($sql, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        return $this->_db->query($sql, $values);
+    }
+
+    function getClientSettings($clientID)
+    {
+        /* Make sure we have a valid database connection. */
+        $this->_connect();
+
+        $clients = Hermes::listClients();
+        if (empty($clientID) || !isset($clients[$clientID])) {
+            return PEAR::raiseError('Does not exist');
+        }
+
+        $sql = 'SELECT clientjob_id, clientjob_enterdescription,' .
+               ' clientjob_exportid FROM hermes_clientjobs' .
+               ' WHERE clientjob_id = ?';
+        $values = array($clientID);
+
+        Horde::logMessage($sql, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        $clientJob = $this->_db->getAssoc($sql, false, $values);
+        if (is_a($clientJob, 'PEAR_Error')) {
+            return $clientJob;
+        }
+
+        if (isset($clientJob[$clientID])) {
+            $settings = array('id' => $clientID,
+                              'enterdescription' => $clientJob[$clientID][0],
+                              'exportid' => $clientJob[$clientID][1]);
+        } else {
+            $settings = array('id' => $clientID,
+                              'enterdescription' => 1,
+                              'exportid' => null);
+        }
+
+        $settings['name'] = $clients[$clientID];
+        return $settings;
+    }
+
+    function updateClientSettings($clientID, $enterdescription = 1, $exportid = null)
+    {
+        /* Make sure we have a valid database connection. */
+        $this->_connect();
+
+        if (empty($exportid)) {
+            $exportid = null;
+        }
+
+        $sql = 'SELECT clientjob_id FROM hermes_clientjobs' .
+               ' WHERE clientjob_id = ?';
+        $values = array($clientID);
+
+        Horde::logMessage($sql, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        if ($this->_db->getOne($sql, $values) !== $clientID) {
+            $sql = 'INSERT INTO hermes_clientjobs (clientjob_id,' .
+                   ' clientjob_enterdescription, clientjob_exportid)' .
+                   ' VALUES (?, ?, ?)';
+            $values = array($clientID, (int)$enterdescription, $exportid);
+        } else {
+            $sql = 'UPDATE hermes_clientjobs SET' .
+                   ' clientjob_exportid = ?, clientjob_enterdescription = ?' .
+                   ' WHERE clientjob_id = ?';
+            $values = array($exportid, (int)$enterdescription, $clientID);
+        }
+
+        Horde::logMessage($sql, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        return $this->_db->query($sql, $values);
+    }
+
+    function purge()
+    {
+        global $conf;
+
+        /* Make sure we have a valid database connection. */
+        $this->_connect();
+
+        $query = 'DELETE FROM hermes_timeslices' .
+                 ' WHERE timeslice_exported = ? AND timeslice_date < ?';
+        $values = array(1, mktime(0, 0, 0, date('n'),
+                                  date('j') - $conf['time']['days_to_keep']));
+        return $this->_db->query($query, $values);
+    }
+
+    /**
+     * Attempts to open a persistent connection to the SQL server.
+     *
+     * @return boolean  True on success; exits (Horde::fatal()) on error.
+     */
+    function _connect()
+    {
+        if (!$this->_connected) {
+            Horde::assertDriverConfig($this->_params, 'storage',
+                array('phptype'));
+
+            if (!isset($this->_params['database'])) {
+                $this->_params['database'] = '';
+            }
+            if (!isset($this->_params['username'])) {
+                $this->_params['username'] = '';
+            }
+            if (!isset($this->_params['hostspec'])) {
+                $this->_params['hostspec'] = '';
+            }
+
+            /* Connect to the SQL server using the supplied parameters. */
+            include_once 'DB.php';
+            $this->_db = &DB::connect($this->_params,
+                                      array('persistent' => !empty($this->_params['persistent'])));
+            if (is_a($this->_db, 'PEAR_Error')) {
+                Horde::fatal($this->_db, __FILE__, __LINE__);
+            }
+
+            // Set DB portability options.
+            switch ($this->_db->phptype) {
+            case 'mssql':
+                $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM);
+                break;
+            default:
+                $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS);
+            }
+
+            $this->_connected = true;
+        }
+
+        return true;
+    }
+
+    /**
+     * Disconnect from the SQL server and clean up the connection.
+     *
+     * @return boolean  True on success, false on failure.
+     */
+    function _disconnect()
+    {
+        if ($this->_connected) {
+            $this->_connected = false;
+            return $this->_db->disconnect();
+        }
+
+        return true;
+    }
+
+}
diff --git a/hermes/lib/Forms/Deliverable.php b/hermes/lib/Forms/Deliverable.php
new file mode 100644 (file)
index 0000000..a25b853
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+
+require_once 'Horde/Form.php';
+
+/**
+ * DeliverableClientSelector - Form for selecting client on deliverables screen
+ *
+ * $Horde: hermes/lib/Forms/Deliverable.php,v 1.7 2009/01/06 17:50:09 jan Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ *
+ * @author  Jason M. Felice <jason.m.felice@gmail.com>
+ * @package Hermes
+ */
+class DeliverableClientSelector extends Horde_Form {
+
+    function DeliverableClientSelector(&$vars)
+    {
+        parent::Horde_Form($vars, _("Select Client"));
+
+        require 'Horde/Form/Action.php';
+        $action = &Horde_Form_Action::factory('submit');
+        list($clienttype, $clientparams) = $this->getClientType();
+
+        $cli = &$this->addVariable(_("Client"), 'client_id', $clienttype, true, false, null, $clientparams);
+        $cli->setAction($action);
+        $this->setButtons(_("Edit Deliverables"));
+    }
+
+    function getClientType()
+    {
+        $clients = Hermes::listClients();
+        if (is_a($clients, 'PEAR_Error')) {
+            return array('invalid', array(sprintf(_("An error occurred listing clients: %s"),
+                                                  $clients->getMessage())));
+        } elseif (count($clients)) {
+            return array('enum', array($clients));
+        } else {
+            return array('invalid', array(_("There are no clients which you have access to.")));
+        }
+    }
+
+}
+
+class DeliverableForm extends Horde_Form {
+
+    function DeliverableForm(&$vars)
+    {
+        parent::Horde_Form($vars, _("Deliverable Detail"));
+
+        $this->addHidden('', 'deliverable_id', 'text', false);
+        $this->addHidden('', 'client_id', 'text', false);
+        $this->addHidden('', 'parent', 'text', false);
+
+        $this->addVariable(_("Display Name"), 'name', 'text', true);
+        $this->addVariable(_("Active?"), 'active', 'boolean', true);
+        $this->addVariable(_("Estimated Hours"), 'estimate', 'number', false);
+        $this->addVariable(_("Description"), 'description', 'longtext', false);
+    }
+
+}
diff --git a/hermes/lib/Forms/Export.php b/hermes/lib/Forms/Export.php
new file mode 100644 (file)
index 0000000..028f1fe
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+/**
+ * @package Hermes
+ *
+ * $Horde: hermes/lib/Forms/Export.php,v 1.9 2009/12/01 12:52:41 jan Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ *
+ */
+
+/**
+ * Horde_Form
+ */
+require_once 'Horde/Form.php';
+
+/**
+ * ExportForm:: is the export form which appears with search results on
+ * the search screen.
+ *
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ * @author  Jason M. Felice <jason.m.felice@gmail.com>
+ * @package Hermes
+ */
+class ExportForm extends Horde_Form {
+
+    var $_useFormToken = false;
+
+    function ExportForm(&$vars)
+    {
+        global $perms;
+
+        parent::Horde_Form($vars, _("Export Search Results"));
+
+        $formats = array('hermes_csv' => _("Comma-Separated Variable (.csv)"),
+                         'hermes_xls' => _("Microsoft Excel (.xls)"),
+                         'iif' => _("QuickBooks (.iif)"),
+                         'hermes_tsv' => _("Tab-Separated Variable (.tsv, .txt)"),
+                         );
+
+        $this->addVariable(_("Select the export format"), 'format', 'enum',
+                           true, false, null, array($formats));
+
+        if ($perms->hasPermission('hermes:review', Horde_Auth::getAuth(),
+                                  Horde_Perms::EDIT)) {
+            $yesno = array('yes' => _("Yes"),
+                           'no' => _("No"));
+            $var = &$this->addVariable(_("Mark the time as exported?"),
+                                       'mark_exported', 'enum', true, false,
+                                       null, array($yesno));
+            $var->setDefault('no');
+        }
+
+        $this->setButtons(_("Export"));
+    }
+
+}
diff --git a/hermes/lib/Forms/Search.php b/hermes/lib/Forms/Search.php
new file mode 100644 (file)
index 0000000..ec62ec2
--- /dev/null
@@ -0,0 +1,216 @@
+<?php
+/**
+ * @package Hermes
+ *
+ * $Horde: hermes/lib/Forms/Search.php,v 1.15 2010/04/21 17:33:10 jan Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ *
+ */
+
+/** Horde_Array */
+require_once 'Horde/Array.php';
+
+/** Horde_Form */
+require_once 'Horde/Form.php';
+
+/**
+ * Hermes time search form.
+ *
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ * @package Hermes
+ */
+class SearchForm extends Horde_Form {
+
+    var $_useFormToken = false;
+
+    function SearchForm(&$vars)
+    {
+        parent::Horde_Form($vars, _("Search For Time"));
+
+        if ($GLOBALS['perms']->hasPermission('hermes:review', Horde_Auth::getAuth(), Horde_Perms::SHOW)) {
+            $type = Hermes::getEmployeesType();
+            $this->addVariable(_("Employees"), 'employees', $type[0], false,
+                               false, null, $type[1]);
+        }
+
+        require_once 'Horde/Form/Action.php';
+        $type = $this->getClientsType();
+        $cli = &$this->addVariable(_("Clients"), 'clients', $type[0], false, false,
+                                   null, $type[1]);
+        $cli->setAction(Horde_Form_Action::factory('submit'));
+        $cli->setOption('trackchange', true);
+
+        $type = $this->getJobTypesType();
+        $this->addVariable(_("Job Types"), 'jobtypes', $type[0], false, false,
+                           null, $type[1]);
+
+        $this->addVariable(_("Cost Objects"), 'costobjects', 'multienum',
+                           false, false, null,
+                           array($this->getCostObjectType($vars)));
+
+        $this->addVariable(_("Do not include entries before"), 'start',
+                           'monthdayyear', false, false, null,
+                           array(date('Y') - 10));
+        $this->addVariable(_("Do not include entries after"), 'end',
+                           'monthdayyear', false, false, null,
+                           array(date('Y') - 10));
+
+        $states = array(''  => '',
+                        '1' => _("Yes"),
+                        '0' => _("No"));
+        $this->addVariable(_("Submitted?"), 'submitted', 'enum', false, false,
+                           null, array($states));
+
+        $this->addVariable(_("Exported?"), 'exported', 'enum', false, false,
+                           null, array($states));
+
+        $this->addVariable(_("Billable?"), 'billable', 'enum', false, false,
+                           null, array($states));
+
+        $this->setButtons(_("Search"));
+    }
+
+    function getClientsType()
+    {
+        $clients = Hermes::listClients();
+        if (is_a($clients, 'PEAR_Error')) {
+            return array('invalid', array(sprintf(_("An error occurred listing clients: %s"),
+                                           $clients->getMessage())));
+        } else {
+            $clients = array('' => _("- - None - -")) + $clients;
+            return array('multienum', array($clients));
+        }
+    }
+
+    function getJobTypesType()
+    {
+        global $hermes;
+
+        $types = $hermes->listJobTypes();
+        if (is_a($types, 'PEAR_Error')) {
+            return array('invalid', array(sprintf(_("An error occurred listing job types: %s"),
+                                           $types->getMessage())));
+        } else {
+            $values = array();
+            foreach ($types as $id => $type) {
+                $values[$id] = $type['name'];
+                if (empty($type['enabled'])) {
+                    $values[$id] .= _(" (DISABLED)");
+                }
+            }
+            return array('multienum', array($values));
+        }
+    }
+
+    function getCostObjectType($vars)
+    {
+        global $hermes, $registry;
+
+        $clients = $vars->get('clients');
+        if (count($clients) == 0){
+            $clients = array('');
+        }
+
+        $costobjects = array();
+        foreach ($clients as $client) {
+            $criteria = array('user' => Horde_Auth::getAuth(),
+                              'active' => true,
+                              'client_id' => $client);
+
+            foreach ($registry->listApps() as $app) {
+                if ($registry->hasMethod('listCostObjects', $app)) {
+                    $res = $registry->callByPackage($app, 'listCostObjects',
+                                                    array($criteria));
+                    if (is_a($res, 'PEAR_Error')) {
+                        global $notification;
+                        $notification->push(sprintf(_("Error retrieving cost objects from \"%s\": %s"), $registry->get('name', $app), $res->getMessage()), 'horde.error');
+                        continue;
+                    }
+                    foreach (array_keys($res) as $catkey) {
+                        foreach (array_keys($res[$catkey]['objects']) as $okey){
+                            $res[$catkey]['objects'][$okey]['id'] = $app . ':' .
+                                $res[$catkey]['objects'][$okey]['id'];
+                        }
+                    }
+                    $costobjects = array_merge($costobjects, $res);
+                }
+            }
+        }
+
+        $elts = array();
+        $counter = 0;
+        foreach ($costobjects as $category) {
+            Horde_Array::arraySort($category['objects'], 'name');
+            foreach ($category['objects'] as $object) {
+                $name = $object['name'];
+                if (Horde_String::length($name) > 80) {
+                    $name = Horde_String::substr($name, 0, 76) . ' ...';
+                }
+                $elts[$object['id']] = $name;
+            }
+        }
+
+        return $elts;
+    }
+
+
+    function getSearchCriteria(&$vars)
+    {
+        require_once 'Date.php';
+
+        if (!$this->isValid() || !$this->isSubmitted()) {
+            return null;
+        }
+        $this->getInfo($vars, $info);
+
+        $criteria = array();
+        if ($GLOBALS['perms']->hasPermission('hermes:review', Horde_Auth::getAuth(), Horde_Perms::SHOW)) {
+            if (!empty($info['employees'])) {
+                $auth = Horde_Auth::singleton($GLOBALS['conf']['auth']['driver']);
+                if (!$auth->capabilities['list']) {
+                    $criteria['employee'] = explode(',', $info['employees']);
+                } else {
+                    $criteria['employee'] = $info['employees'];
+                }
+            }
+        } else {
+            $criteria['employee'] = Horde_Auth::getAuth();
+        }
+        if (!empty($info['clients'])) {
+            $criteria['client'] = $info['clients'];
+        }
+        if (!empty($info['jobtypes'])) {
+            $criteria['jobtype'] = $info['jobtypes'];
+        }
+        if (!empty($info['costobjects'])) {
+            $criteria['costobject'] = $info['costobjects'];
+        }
+        if (!empty($info['start'])) {
+            $dt = new Date($info['start']);
+            $criteria['start'] = $dt->getDate(DATE_FORMAT_UNIXTIME);
+        }
+        if (!empty($info['end'])) {
+            $dt = new Date($info['end']);
+            $dt->setHour(23);
+            $dt->setMinute(59);
+            $dt->setSecond(59);
+            $criteria['end'] = $dt->getDate(DATE_FORMAT_UNIXTIME);
+        }
+        if (isset($info['submitted']) && $info['submitted'] != '') {
+            $criteria['submitted'] = $info['submitted'];
+        }
+        if (isset($info['exported']) && $info['exported'] != '') {
+            $criteria['exported'] = $info['exported'];
+        }
+        if (isset($info['billable']) && $info['billable'] != '') {
+            $criteria['billable'] = $info['billable'];
+        }
+
+        return $criteria;
+    }
+
+}
diff --git a/hermes/lib/Forms/Time.php b/hermes/lib/Forms/Time.php
new file mode 100644 (file)
index 0000000..ae49019
--- /dev/null
@@ -0,0 +1,328 @@
+<?php
+/**
+ * @package Hermes
+ *
+ * $Horde: hermes/lib/Forms/Time.php,v 1.23 2009/07/08 18:29:08 slusarz Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ */
+
+/** Horde_Array */
+require_once 'Horde/Array.php';
+
+/** Horde_Form */
+require_once 'Horde/Form.php';
+
+/**
+ * TimeForm abstract class.
+ *
+ * Hermes forms can extend this to gain access to shared functionality.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @package Hermes
+ */
+class TimeForm extends Horde_Form {
+
+    function TimeForm(&$vars, $name = null)
+    {
+        parent::Horde_Form($vars, $name);
+    }
+
+    function getJobTypeType()
+    {
+        global $hermes;
+
+        $types = $hermes->listJobTypes(array('enabled' => true));
+        if (is_a($types, 'PEAR_Error')) {
+            return array('invalid', array(sprintf(_("An error occurred listing job types: %s"),
+                                                  $types->getMessage())));
+        } elseif (count($types)) {
+            $values = array();
+            foreach ($types as $id => $type) {
+                $values[$id] = $type['name'];
+            }
+            return array('enum', array($values));
+        } else {
+            return array('invalid', array(_("There are no job types configured.")));
+        }
+    }
+
+    function getClientType()
+    {
+        $clients = Hermes::listClients();
+        if (is_a($clients, 'PEAR_Error')) {
+            return array('invalid', array(sprintf(_("An error occurred listing clients: %s"),
+                                                  $clients->getMessage())));
+        } elseif ($clients) {
+            if (count($clients) > 1) {
+                $clients = array('' => _("--- Select A Client ---")) + $clients;
+            }
+            return array('enum', array($clients));
+        } else {
+            return array('invalid', array(_("There are no clients which you have access to.")));
+        }
+    }
+
+    /**
+     */
+    function getCostObjectType($clientID = null)
+    {
+        global $hermes, $registry;
+
+        /* Check to see if any other active applications are exporting cost
+         * objects to which we might want to bill our time. */
+        $criteria = array('user'   => Horde_Auth::getAuth(),
+                          'active' => true);
+        if (!empty($clientID)) {
+            $criteria['client_id'] = $clientID;
+        }
+
+        $costobjects = array();
+        foreach ($registry->listApps() as $app) {
+            if (!$registry->hasMethod('listCostObjects', $app)) {
+                continue;
+            }
+
+            $result = $registry->callByPackage($app, 'listCostObjects',
+                                               array($criteria));
+            if (is_a($result, 'PEAR_Error')) {
+                global $notification;
+                $notification->push(sprintf(_("Error retrieving cost objects from \"%s\": %s"), $registry->get('name', $app), $result->getMessage()), 'horde.error');
+                continue;
+            }
+
+            foreach (array_keys($result) as $catkey) {
+                foreach (array_keys($result[$catkey]['objects']) as $okey){
+                    $result[$catkey]['objects'][$okey]['id'] = $app . ':' .
+                        $result[$catkey]['objects'][$okey]['id'];
+                }
+            }
+
+            if ($app == $registry->getApp()) {
+                $costobjects = array_merge($result, $costobjects);
+            } else {
+                $costobjects = array_merge($costobjects, $result);
+            }
+        }
+
+        $elts = array('' => _("--- No Cost Object ---"));
+        $counter = 0;
+        foreach ($costobjects as $category) {
+            Horde_Array::arraySort($category['objects'], 'name');
+            $elts['category%' . $counter++] = sprintf('--- %s ---', $category['category']);
+            foreach ($category['objects'] as $object) {
+                $name = $object['name'];
+                if (Horde_String::length($name) > 80) {
+                    $name = Horde_String::substr($name, 0, 76) . ' ...';
+                }
+
+                $hours = 0.0;
+                $filter = array('costobject' => $object['id']);
+                if (!empty($GLOBALS['conf']['time']['sum_billable_only'])) {
+                    $filter['billable'] = true;
+                }
+                $result = $hermes->getHours($filter, array('hours'));
+                if (!is_a($result, 'PEAR_Error')) {
+                    foreach ($result as $entry) {
+                        if (!empty($entry['hours'])) {
+                            $hours += $entry['hours'];
+                        }
+                    }
+                }
+
+                /* Show summary of hours versus estimate for this
+                 * deliverable. */
+                if (empty($object['estimate'])) {
+                    $name .= sprintf(_(" (%0.2f hours)"), $hours);
+                } else {
+                    $name .= sprintf(_(" (%d%%, %0.2f of %0.2f hours)"),
+                                     (int)($hours / $object['estimate'] * 100),
+                                     $hours, $object['estimate']);
+                }
+
+                $elts[$object['id']] = $name;
+            }
+        }
+
+        return $elts;
+    }
+
+}
+
+/**
+ * TimeEntryForm Class.
+ *
+ * $Horde: hermes/lib/Forms/Time.php,v 1.23 2009/07/08 18:29:08 slusarz Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @package Hermes
+ */
+class TimeEntryForm extends TimeForm {
+
+    /**
+     * Reference to the form field storing the cost objects.
+     *
+     * @var Horde_Form_Variable
+     */
+    var $_costObjects;
+
+    function TimeEntryForm(&$vars)
+    {
+        global $hermes, $conf;
+
+        if ($vars->exists('id')) {
+            parent::TimeForm($vars, _("Update Time"));
+            $delete_link = Horde::link(Horde_Util::addParameter(Horde::applicationUrl('time.php'), 'delete', $vars->get('id')), _("Delete Entry")) . _("Delete");
+            $this->setExtra('<span class="smallheader">' . $delete_link . '</a></span>');
+        } else {
+            parent::TimeForm($vars, _("New Time"));
+        }
+        $this->setButtons(_("Save"));
+
+        list($clienttype, $clientparams) = $this->getClientType();
+        if ($clienttype == 'enum') {
+            require_once 'Horde/Form/Action.php';
+            $action = &Horde_Form_Action::factory('submit');
+        }
+
+        list($typetype, $typeparams) = $this->getJobTypeType();
+
+        if ($vars->exists('id')) {
+            $this->addHidden('', 'id', 'int', true);
+        }
+
+        if ($vars->exists('url')) {
+            $this->addHidden('', 'url', 'text', true);
+        }
+
+        $var = &$this->addVariable(_("Date"), 'date', 'monthdayyear', true,
+                                   false, null, array(date('Y') - 1));
+        $var->setDefault(date('Y-m-d'));
+
+        $cli = &$this->addVariable(_("Client"), 'client', $clienttype, true, false, null, $clientparams);
+        if (isset($action)) {
+            $cli->setAction($action);
+            $cli->setOption('trackchange', true);
+        }
+
+        $this->addVariable(_("Job Type"), 'type', $typetype, true, false, null, $typeparams);
+
+        $this->_costObjects = &$this->addVariable(
+            _("Cost Object"), 'costobject', 'enum', false, false, null,
+            array(array()));
+
+        $this->addVariable(_("Hours"), 'hours', 'number', true);
+
+        if ($conf['time']['choose_ifbillable']) {
+            $yesno = array(1 => _("Yes"), 0 => _("No"));
+            $this->addVariable(_("Billable?"), 'billable', 'enum', true, false, null, array($yesno));
+        }
+
+        if ($vars->exists('client')) {
+            $info = $hermes->getClientSettings($vars->get('client'));
+            if (!is_a($info, 'PEAR_Error') && !$info['enterdescription']) {
+                $vars->set('description', _("See Attached Timesheet"));
+            }
+        }
+        $descvar = &$this->addVariable(_("Description"), 'description', 'longtext', true, false, null, array(4, 60));
+
+        $this->addVariable(_("Additional Notes"), 'note', 'longtext', false, false, null, array(4, 60));
+    }
+
+    function setCostObjects($vars)
+    {
+        $this->_costObjects->type->setValues(
+            $this->getCostObjectType($vars->get('client')));
+    }
+
+}
+
+/**
+ * TimeReviewForm Class.
+ *
+ * $Horde: hermes/lib/Forms/Time.php,v 1.23 2009/07/08 18:29:08 slusarz Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ *
+ * @author  Jay 'Eraserhead' Felice <jason.m.felice@gmail.com>
+ * @package Hermes
+ */
+class TimeReviewForm extends TimeForm {
+
+    function TimeReviewForm(&$vars)
+    {
+        global $hermes, $conf;
+
+        parent::TimeForm($vars, _("Update Submitted Time"));
+        $this->setButtons(_("Update time"));
+
+        list($clienttype, $clientparams) = $this->getClientType();
+        if ($clienttype == 'enum') {
+            $map = array();
+            $clients = Hermes::listClients();
+            foreach ($clients as $id => $name) {
+                $info = $hermes->getClientSettings($id);
+                if (!is_a($info, 'PEAR_Error')) {
+                    $map[$id] = $info['enterdescription'] ? '' : _("See Attached Timesheet");
+                } else {
+                    $map[$id] = '';
+                }
+            }
+
+            require_once 'Horde/Form/Action.php';
+            $action = &Horde_Form_Action::factory('conditional_setvalue',
+                                                  array('map' => $map,
+                                                        'target' => 'description'));
+        }
+
+        list($typetype, $typeparams) = $this->getJobTypeType();
+
+        $this->addHidden('', 'id', 'int', true);
+
+        $employees = array();
+
+        require_once 'Horde/Identity.php';
+        $auth = Horde_Auth::singleton($conf['auth']['driver']);
+        $users = $auth->listUsers();
+        if (!is_a($users, 'PEAR_Error')) {
+            foreach ($users as $user) {
+                $identity = &Identity::singleton('none', $user);
+                $employees[$user] = $identity->getValue('fullname');
+            }
+        }
+
+        $this->addVariable(_("Employee"), 'employee', 'enum', true, false, null, array($employees));
+
+        $var = &$this->addVariable(_("Date"), 'date', 'monthdayyear', true);
+        $var->setDefault(date('y-m-d'));
+
+
+        $cli = &$this->addVariable(_("Client"), 'client', $clienttype, true, false, null, $clientparams);
+        if (isset($action)) {
+            $cli->setAction($action);
+        }
+
+        $this->addVariable(_("Job Type"), 'type', $typetype, true, false, null, $typeparams);
+        $this->addVariable(_("Hours"), 'hours', 'number', true);
+
+        if ($conf['time']['choose_ifbillable']) {
+            $yesno = array(1 => _("Yes"), 0 => _("No"));
+            $this->addVariable(_("Billable?"), 'billable', 'enum', true, false, null, array($yesno));
+        }
+
+        $this->addVariable(_("Description"), 'description', 'longtext', true, false, null, array(4, 60));
+        $this->addVariable(_("Additional Notes"), 'note', 'longtext', false, false, null, array(4, 60));
+    }
+
+}
diff --git a/hermes/lib/Hermes.php b/hermes/lib/Hermes.php
new file mode 100644 (file)
index 0000000..a18439b
--- /dev/null
@@ -0,0 +1,238 @@
+<?php
+/**
+ * Hermes Base Class.
+ *
+ * $Horde: hermes/lib/Hermes.php,v 1.66 2009/12/10 17:42:31 jan Exp $
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @package Hermes
+ */
+class Hermes {
+
+    function &getDriver()
+    {
+        global $conf;
+
+        require_once HERMES_BASE . '/lib/Driver.php';
+        return Hermes_Driver::singleton();
+    }
+
+    function listClients()
+    {
+        static $clients;
+
+        if (is_null($clients)) {
+            $result = $GLOBALS['registry']->call('clients/searchClients', array(array('')));
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+
+            $client_name_field = $GLOBALS['conf']['client']['field'];
+            $clients = array();
+            if (!empty($result)) {
+                $result = $result[''];
+                foreach ($result as $client) {
+                    $clients[$client['id']] = $client[$client_name_field];
+                }
+            }
+
+            uasort($clients, 'strcoll');
+        }
+
+        return $clients;
+    }
+
+    function getDayOfWeek($timestamp)
+    {
+        // Return 0-6, indicating the position of $timestamp in the
+        // period.
+        $dow = 7 + date('w', $timestamp) - $GLOBALS['prefs']->getValue('start_of_week');
+        return ($dow % 7);
+    }
+
+    /**
+     * Build Hermes' list of menu items.
+     */
+    function getMenu($returnType = 'object')
+    {
+        global $registry, $conf, $perms, $print_link;
+
+        $menu = new Horde_Menu();
+        $menu->add(Horde::applicationUrl('time.php'), _("My _Time"), 'hermes.png', null, null, null, basename($_SERVER['PHP_SELF']) == 'index.php' ? 'current' : null);
+        $menu->add(Horde::applicationUrl('entry.php'), _("_New Time"), 'hermes.png', null, null, null, Horde_Util::getFormData('id') ? '__noselection' : null);
+        $menu->add(Horde::applicationUrl('search.php'), _("_Search"), 'search.png', $registry->getImageDir('horde'));
+
+        if ($conf['time']['deliverables'] && Horde_Auth::isAdmin('hermes:deliverables')) {
+            $menu->add(Horde::applicationUrl('deliverables.php'), _("_Deliverables"), 'hermes.png');
+        }
+
+        if ($conf['invoices']['driver'] && Horde_Auth::isAdmin('hermes:invoicing')) {
+            $menu->add(Horde::applicationUrl('invoicing.php'), _("_Invoicing"), 'invoices.png');
+        }
+
+        /* Print. */
+        if ($conf['menu']['print'] && isset($print_link)) {
+            $menu->add($print_link, _("_Print"), 'print.png', $registry->getImageDir('horde'), '_blank', 'popup(this.href); return false;', '__noselection');
+        }
+
+        /* Administration. */
+        if (Horde_Auth::isAdmin()) {
+            $menu->add(Horde::applicationUrl('admin.php'), _("_Admin"), 'hermes.png');
+        }
+
+        if ($returnType == 'object') {
+            return $menu;
+        } else {
+            return $menu->render();
+        }
+    }
+
+    function canEditTimeslice($id)
+    {
+        global $hermes;
+
+        if ($GLOBALS['perms']->hasPermission('hermes:review', Horde_Auth::getAuth(), Horde_Perms::EDIT)) {
+            return true;
+        }
+
+        $hours = $hermes->getHours(array('id' => $id));
+        if (!is_array($hours) || count($hours) != 1) {
+            return false;
+        }
+        $slice = $hours[0];
+
+        // We can edit our own time if it hasn't been submitted.
+        if ($slice['employee'] == Horde_Auth::getAuth() && !$slice['submitted']) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Rewrite an hours array into a format useable by Horde_Data::
+     *
+     * @param array $hours          This is an array of the results from
+     *                              $hermes->getHours().
+     * @return array an array suitable for Horde_Data::
+     */
+    function makeExportHours($hours)
+    {
+        if (is_null($hours)) {
+            return null;
+        }
+        require_once 'Horde/Identity.php';
+
+        $clients = Hermes::listClients();
+        $namecache = array();
+        for ($i = 0; $i < count($hours); $i++) {
+            $timeentry = &$hours[$i];
+
+            $timeentry['item'] = $timeentry['_type_name'];
+            if (isset($clients[$timeentry['client']])) {
+                $timeentry['client'] = $clients[$timeentry['client']];
+            }
+
+            $emp = &$timeentry['employee'];
+            if (isset($namecache[$emp])) {
+                $emp = $namecache[$emp];
+            } else {
+                $ident = &Identity::singleton('none', $emp);
+                $fullname = $ident->getValue('fullname');
+                if ($fullname) {
+                    $namecache[$emp] = $emp = $fullname;
+                } else {
+                    $namecache[$emp] = $emp;
+                }
+            }
+        }
+
+        return $hours;
+    }
+
+    /**
+     * Get form control type for users.
+     *
+     * What type of control we use depends on whether the Auth driver has list
+     * capability.
+     *
+     * @param string $enumtype  The type to return if we have list capability
+     *                          (should be either 'enum' or 'multienum').
+     *
+     * @return array A two-element array of the type and the type's parameters.
+     */
+    function getEmployeesType($enumtype = 'multienum')
+    {
+        require_once 'Horde/Identity.php';
+        $auth = Horde_Auth::singleton($GLOBALS['conf']['auth']['driver']);
+        if (!$auth->capabilities['list']) {
+            return array('text', array());
+        }
+        $users = $auth->listUsers();
+        if (is_a($users, 'PEAR_Error')) {
+            return array('invalid',
+                         array(sprintf(_("An error occurred listing users: %s"),
+                                       $users->getMessage())));
+        }
+
+        $employees = array();
+        foreach ($users as $user) {
+            $identity = &Identity::singleton('none', $user);
+            $label = $identity->getValue('fullname');
+            if (empty($label)) {
+                $label = $user;
+            }
+            $employees[$user] = $label;
+        }
+
+        return array($enumtype, array($employees));
+    }
+
+    function getCostObjectByID($id)
+    {
+        static $cost_objects;
+
+        if (strpos($id, ':') !== false) {
+            list($app, $app_id) = explode(':', $id, 2);
+
+            if (!isset($cost_objects[$app])) {
+                $results = $GLOBALS['registry']->callByPackage($app, 'listCostObjects', array(array()));
+                if (is_a($results, 'PEAR_Error')) {
+                    return $results;
+                }
+                $cost_objects[$app] = $results;
+            }
+
+            foreach (array_keys($cost_objects[$app]) as $catkey) {
+                foreach (array_keys($cost_objects[$app][$catkey]['objects']) as $objkey) {
+                    if ($cost_objects[$app][$catkey]['objects'][$objkey]['id'] == $app_id) {
+                        return $cost_objects[$app][$catkey]['objects'][$objkey];
+                    }
+                }
+            }
+        }
+
+        return PEAR::raiseError(_("Not found."));
+    }
+
+    function tabs()
+    {
+        /* Build search mode tabs. */
+        $sUrl = Horde::selfUrl();
+        $tabs = new Horde_Ui_Tabs('search_mode', Horde_Variables::getDefaultVariables());
+        $tabs->addTab(_("Summary"), $sUrl, 'summary');
+        $tabs->addTab(_("By Date"), $sUrl, 'date');
+        $tabs->addTab(_("By Employee"), $sUrl, 'employee');
+        $tabs->addTab(_("By Client"), $sUrl, 'client');
+        $tabs->addTab(_("By Job Type"), $sUrl, 'jobtype');
+        $tabs->addTab(_("By Cost Object"), $sUrl, 'costobject');
+        if ($mode = Horde_Util::getFormData('search_mode')) {
+            $_SESSION['hermes_search_mode'] = $mode;
+        }
+        if (!isset($_SESSION['hermes_search_mode'])) {
+            $_SESSION['hermes_search_mode'] = 'summary';
+        }
+        return $tabs->render($_SESSION['hermes_search_mode']);
+    }
+
+}
diff --git a/hermes/lib/Table.php b/hermes/lib/Table.php
new file mode 100644 (file)
index 0000000..5025aef
--- /dev/null
@@ -0,0 +1,244 @@
+<?php
+/**
+ * The Horde_UI_Table:: class displays and allows manipulation of tabular
+ * data.
+ *
+ * $Horde: hermes/lib/Table.php,v 1.10 2009/12/10 17:42:31 jan Exp $
+ *
+ * Copyright 2001 Robert E. Coyle <robertecoyle@hotmail.com>
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ *
+ * @package Horde_Ui
+ */
+class Horde_Ui_Table extends Horde_Ui_Widget {
+
+    /**
+     * Data loaded from the getTableMetaData API.
+     *
+     * @access private
+     * @var array
+     */
+    var $_metaData = null;
+
+    /**
+     * @var array
+     */
+    var $_formVars = array();
+
+    function getMetaData()
+    {
+        if (is_null($this->_metaData)) {
+            list($app, $name) = explode('/', $this->_config['name']);
+            $args = array($name, $this->_config['params']);
+            $this->_metaData = $GLOBALS['registry']->callByPackage(
+                $app, 'getTableMetaData', $args);
+            if (is_a($this->_metaData, 'PEAR_Error')) {
+                return $this->_metaData;
+            }
+
+            // We need to make vars for the columns.
+            foreach ($this->_metaData['sections'] as $secname => $section) {
+                foreach ($section['columns'] as $col) {
+                    $title = isset($col['title']) ? $col['title'] : '';
+                    $typename = isset($col['type']) ? $col['type'] : 'text';
+                    $params = isset($col['params']) ? $col['params'] : array();
+
+                    // Column types which begin with % are pseudo-types handled
+                    // directly.
+                    if (substr($typename, 0, 1) != '%') {
+                        // This type needs to be assigned by reference!
+                        $type = &Horde_Form::getType($typename, $params);
+                        $var = new Horde_Form_Variable($title, $col['name'],
+                                                       $type, false, true, '');
+                        $this->_formVars[$secname][$col['name']] = $var;
+                    }
+                }
+            }
+        }
+
+        return $this->_metaData;
+    }
+
+    function _getData($range = null)
+    {
+        if (is_null($range)) {
+            $range = array();
+            foreach (array_keys($this->_metaData['sections']) as $secname) {
+                $range[$secname] = array(
+                    0,
+                    $this->_metaData['sections'][$secname]['rows']);
+            }
+        }
+
+        list($app, $name) = explode('/', $this->_config['name']);
+        $args = array($name, $this->_config['params'], $range);
+        return $GLOBALS['registry']->callByPackage($app, 'getTableData', $args);
+    }
+
+    /**
+     * Count the number of columns in this table.
+     *
+     * Returns the largest column count of any section, taking into account
+     * 'colspan' attributes.
+     *
+     * @return mixed number of columns or PEAR_Error
+     */
+    function getColumnCount()
+    {
+        $res = $this->getMetaData();
+        if (is_a($res, 'PEAR_Error')) {
+            return $res;
+        }
+        $colcount = 0;
+        foreach ($this->_metaData['sections'] as $section) {
+            $sec_colcount = 0;
+            foreach ($section['columns'] as $col) {
+                if (isset($col['colspan'])) {
+                    $sec_colcount += $col['colspan'];
+                } else {
+                    $sec_colcount++;
+                }
+            }
+            if ($sec_colcount > $colcount) {
+                $colcount = $sec_colcount;
+            }
+        }
+
+        return $colcount;
+    }
+
+    /**
+     * Render the table.
+     */
+    function render()
+    {
+        global $notification;
+
+        $result = $this->getMetaData();
+        if (is_a($result, 'PEAR_Error')) {
+            $notification->push($result, 'horde.error');
+            return false;
+        }
+
+        $varRenderer = new Horde_Ui_VarRenderer_Html();
+
+        $html = '<h1 class="header">';
+
+        // Table title.
+        if (isset($this->_config['title'])) {
+            $html .= $this->_config['title'];
+        } else {
+            $html .= _("Table");
+        }
+
+        // Hook for icons and things
+        if (isset($this->_config['title_extra'])) {
+            $html .= $this->_config['title_extra'];
+        }
+
+        $html .= '</h1>';
+
+        /*
+        //
+        // Export icon.  We store the parameters in the session so that smart
+        // users can't hack it (in Hermes, you could make it show other
+        // people's time, for example).
+        //
+        $id = $this->_config['name'] . ':' . $this->_name;
+        $_SESSION['horde']['tables'][$id] = $this->_config;
+        $exportlink = Horde::url($GLOBALS['registry']->get('webroot', 'horde') .
+                                 '/services/table/export.php');
+        $exportlink = Horde_Util::addParameter($exportlink, array('id' => $id));
+
+        $html .= ' &nbsp;' . Horde::link($exportlink, _("Export Data")) .
+                 Horde::img('data.png', _("Export Data"), 'hspace="2"',
+                            $GLOBALS['registry']->getImageDir('horde')) .
+                 '</a>';
+        */
+
+        // Column titles.
+        $html .= '<table class="time striped" id="hermes_time" cellspacing="0"><thead><tr class="item">';
+        foreach ($this->_metaData['sections']['data']['columns'] as $col) {
+            $html .= '<th' . (isset($col['colspan']) ?
+                              (' colspan="' . $col['colspan'] . '"') :
+                              '') . '>' . $col['title'] . '</th>';
+        }
+        $html .= '</tr></thead>';
+
+        // Display data.
+        $data = $this->_getData();
+        if (is_a($data, 'PEAR_Error')) {
+            $notification->push($data, 'horde.error');
+            $data = array();
+        }
+
+        foreach ($this->_metaData['sections'] as $secname => $section) {
+            if (empty($data[$secname])) {
+                continue;
+            }
+
+            /* Open the table section, either a tbody or the tfoot. */
+            $html .= ($secname == 'footer') ? '<tfoot>' : '<tbody>';
+
+            /* This Horde_Variables object is populated for each table row
+             * so that we can use the Horde_Ui_VarRenderer. */
+            $vars = new Horde_Variables();
+            $form = null;
+            foreach ($data[$secname] as $row) {
+                $html .= '<tr>';
+                foreach ($row as $key => $value) {
+                    $vars->set($key, $value);
+                }
+                foreach ($section['columns'] as $col) {
+                    $value = null;
+                    if (isset($row[$col['name']])) {
+                        $value = $row[$col['name']];
+                    }
+                    $align = '';
+                    if (isset($col['align'])) {
+                        $align = ' align="' . htmlspecialchars($col['align']) . '"';
+                    }
+                    $colspan = '';
+                    if (isset($col['colspan'])) {
+                        $colspan = ' colspan="' .
+                                   htmlspecialchars($col['colspan']) . '"';
+                    }
+                    $html .= "<td$align$colspan";
+                    if (!empty($col['nobr'])) {
+                        $html .= ' class="nowrap"';
+                    }
+                    $html .= '>';
+                    // XXX: Should probably be done at the <tr> with a class.
+                    if (!empty($row['strong'])) {
+                        $html .= '<strong>';
+                    }
+                    if (isset($col['type']) && substr($col['type'], 0, 1) == '%') {
+                        switch ($col['type']) {
+                        case '%html':
+                            if (!empty($row[$col['name']])) {
+                                $html .= $row[$col['name']];
+                            }
+                            break;
+                        }
+                    } else {
+                        $html .= $varRenderer->render($form, $this->_formVars[$secname][$col['name']], $vars);
+                    }
+                    if (!empty($row['strong'])) {
+                        $html .= '</strong>';
+                    }
+                    $html .= '</td>';
+                }
+                $html .= '</tr>';
+            }
+
+            /* Close the table section. */
+            $html .= ($secname == 'footer') ? '</tfoot>' : '</tbody>';
+        }
+
+        Horde::addScriptFile('stripe.js', 'horde', true);
+        return $html . '</table>';
+    }
+
+}
diff --git a/hermes/lib/api.php b/hermes/lib/api.php
new file mode 100644 (file)
index 0000000..7cd8ca5
--- /dev/null
@@ -0,0 +1,453 @@
+<?php
+/**
+ * Hermes external API interface.
+ *
+ * $Horde: hermes/lib/api.php,v 1.45 2009/07/08 18:29:07 slusarz Exp $
+ *
+ * This file defines Hermes's external API interface. Other applications
+ * can interact with Hermes through this API.
+ *
+ * @package Hermes
+ */
+
+$_services['perms'] = array(
+    'args' => array(),
+    'type' => '{urn:horde}stringArray');
+
+$_services['getTableMetaData'] = array(
+    'args' => array('name' => 'string', 'params' => '{urn:horde}stringArray'),
+    'type' => '{urn:horde}stringArray');
+
+$_services['getTableData'] = array(
+    'args' => array('name' => 'string', 'params' => '{urn:horde}stringArray'),
+    'type' => '{urn:horde}stringArray');
+
+$_services['listCostObjects'] = array(
+    'args' => array('criteria' => '{urn:horde}hash'),
+    'type' => '{urn:horde}array');
+
+$_services['listJobTypes'] = array(
+    'arga' => array('criteria' => '{urn:horde}hash'),
+    'type' => '{urn:horde}array');
+
+$_services['listClients'] = array(
+    'arga' => array(),
+    'type' => '{urn:horde}array');
+
+$_services['recordTime'] = array(
+    'arga' => array('date' => 'int',
+                    'client' => 'string',
+                    'jobType' => 'int',
+                    'costObject' => 'int',
+                    'hours' => 'string', // Can not use 'float'
+                    'billable' => 'boolean',
+                    'description' => 'string',
+                    'notes' => 'string'),
+    'type' => 'boolean'); // FIXME: Is there something more useful to return?
+
+function _hermes_perms()
+{
+    $perms = array();
+    $perms['tree']['hermes']['review'] = array();
+    $perms['title']['hermes:review'] = _("Time Review Screen");
+    $perms['tree']['hermes']['deliverables'] = array();
+    $perms['title']['hermes:deliverables'] = _("Deliverables");
+    $perms['tree']['hermes']['invoicing'] = array();
+    $perms['title']['hermes:invoicing'] = _("Invoicing");
+
+    return $perms;
+}
+
+function _hermes_getTableMetaData($name, $params)
+{
+    require_once dirname(__FILE__) . '/base.php';
+
+    switch ($name) {
+    case 'hours':
+        $emptype = Hermes::getEmployeesType('enum');
+        $clients = Hermes::listClients();
+        $hours = $GLOBALS['hermes']->getHours($params);
+        $yesno = array(1 => _("Yes"),
+                       0 => _("No"));
+
+        $columns = array(
+            array('name'   => 'icons',
+                  'title'  => '',
+                  'type'   => '%html',
+                  'nobr'   => true),
+            array('name'   => 'checkbox',
+                  'title'  => '',
+                  'type'   => '%html',
+                  'nobr'   => true),
+            array('name'   => 'date',
+                  'title'  => _("Date"),
+                  'type'   => 'date',
+                  'params' => array($GLOBALS['prefs']->getValue('date_format')),
+                  'nobr'   => true),
+            array('name'   => 'employee',
+                  'title'  => _("Employee"),
+                  'type'   => $emptype[0],
+                  'params' => $emptype[1]),
+            array('name'   => 'client',
+                  'title'  => _("Client"),
+                  'type'   => 'enum',
+                  'params' => array($clients)),
+            array('name'   => '_type_name',
+                  'title'  => _("Job Type")),
+            array('name'   => '_costobject_name',
+                  'title'  => _("Cost Object")),
+            array('name'   => 'hours',
+                  'title'  => _("Hours"),
+                  'type'   => 'number',
+                  'align'  => 'right'));
+        if ($GLOBALS['conf']['time']['choose_ifbillable']) {
+            $columns[] =
+                array('name'   => 'billable',
+                      'title'  => _("Bill?"),
+                      'type'   => 'enum',
+                      'params' => array($yesno));
+        }
+        $columns = array_merge($columns, array(
+            array('name'  => 'description',
+                  'title' => _("Description")),
+            array('name'  => 'note',
+                  'title' => _("Notes"))));
+
+        $colspan = 6;
+        if ($GLOBALS['conf']['time']['choose_ifbillable']) {
+            $colspan++;
+        }
+        $fColumns = array(
+            array('name'    => 'approval',
+                  'colspan' => $colspan,
+                  'type'    => '%html',
+                  'align'   => 'right'),
+            array('name'    => 'hours',
+                  'type'    => 'number',
+                  'align'   => 'right'));
+        if ($GLOBALS['conf']['time']['choose_ifbillable']) {
+            $fColumns[] =
+                array('name'   => 'billable',
+                      'type'   => 'enum',
+                      'params' => array($yesno));
+        }
+        $fColumns = array_merge($fColumns, array(
+            array('name' => 'description'),
+            array('name' => 'blank2')));
+
+        return array('title' => _("Search Results"),
+                     'sections' => array(
+                         'data' => array(
+                             'rows' => count($hours),
+                             'columns' => $columns),
+                         'footer' => array(
+                             'rows' => 3,
+                             'strong' => true,
+                             'columns' => $fColumns)));
+
+    default:
+        return PEAR::raiseError(sprintf(_("\"%s\" is not a defined table."),
+                                        $name));
+    }
+}
+
+function _hermes_getTableData($name, $params)
+{
+    require_once dirname(__FILE__) . '/base.php';
+
+    switch ($name) {
+    case 'hours':
+        $time_data = $GLOBALS['hermes']->getHours($params);
+        if (is_a($time_data, 'PEAR_Error')) {
+            return $time_data;
+        }
+
+        $subtotal_column = null;
+        if (isset($_SESSION['hermes_search_mode'])) {
+            switch ($_SESSION['hermes_search_mode']) {
+            case 'date':
+                $subtotal_column = 'date';
+                break;
+
+            case 'employee':
+                $subtotal_column = 'employee';
+                break;
+
+            case 'client':
+                $subtotal_column = '_client_name';
+                break;
+
+            case 'jobtype':
+                $subtotal_column = '_type_name';
+                break;
+
+            case 'costobject':
+                $subtotal_column = '_costobject_name';
+                break;
+            }
+
+            if (!empty($subtotal_column)) {
+                $clients = Hermes::listClients();
+                $column = array();
+                foreach ($time_data as $key => $row) {
+                    if (empty($row['client'])) {
+                        $time_data[$key]['_client_name'] = _("no client");
+                    } elseif (isset($clients[$row['client']])) {
+                        $time_data[$key]['_client_name'] = $clients[$row['client']];
+                    } else {
+                        $time_data[$key]['_client_name'] = $row['client'];
+                    }
+                    $column[$key] = $time_data[$key][$subtotal_column];
+                }
+                array_multisort($column, SORT_ASC, $time_data);
+            }
+        }
+
+        $total_hours = 0.0;
+        $total_billable_hours = 0.0;
+        $subtotal_hours = 0.0;
+        $subtotal_billable_hours = 0.0;
+        $subtotal_control = null;
+
+        $result['data'] = array();
+        foreach ($time_data as $k => $vals) {
+            // Initialize subtotal break value.
+            if (is_null($subtotal_control) && isset($vals[$subtotal_column])) {
+                $subtotal_control = $vals[$subtotal_column];
+            }
+
+            if (!empty($subtotal_column) &&
+                $vals[$subtotal_column] != $subtotal_control) {
+                renderSubtotals($result['data'], $subtotal_hours, $subtotal_billable_hours,
+                                $subtotal_column == 'date' ? strftime("%m/%d/%Y", $subtotal_control) :
+                                $subtotal_control);
+                $subtotal_hours = 0.0;
+                $subtotal_billable_hours = 0.0;
+                $subtotal_control = $vals[$subtotal_column];
+            }
+
+            // Set up edit/delete icons.
+            if (Hermes::canEditTimeslice($vals['id'])) {
+                $edit_link = Horde::applicationUrl('entry.php', true);
+                $edit_link = Horde_Util::addParameter($edit_link, 'id', $vals['id']);
+                $edit_link = Horde_Util::addParameter($edit_link, 'url', Horde::selfUrl(true, true, true));
+
+                $vals['icons'] =
+                    Horde::link($edit_link, _("Edit Entry")) .
+                    Horde::img('edit.png', _("Edit Entry"), '', $GLOBALS['registry']->getImageDir('horde')) . '</a>';
+
+                if (empty($vals['submitted'])) {
+                    $vals['checkbox'] =
+                        '<input type="checkbox" name="item[' .
+                        htmlspecialchars($vals['id']) .
+                        ']" checked="checked" />';
+                } else {
+                    $vals['checkbox'] = '';
+                }
+            }
+
+            // Add to totals.
+            $subtotal_hours += (double)$vals['hours'];
+            $total_hours += (double)$vals['hours'];
+            if ($vals['billable']) {
+                $subtotal_billable_hours += (double)$vals['hours'];
+                $total_billable_hours += (double)$vals['hours'];
+            }
+
+            // Localize hours.
+            $vals['hours'] = sprintf('%.02f', $vals['hours']);
+
+            $result['data'][] = $vals;
+        }
+
+        if (!empty($subtotal_column)) {
+            renderSubtotals($result['data'], $subtotal_hours, $subtotal_billable_hours,
+                            $subtotal_column == 'date' ? strftime("%m/%d/%Y", $subtotal_control) :
+                            $subtotal_control);
+        }
+
+        // Avoid a divide by zero.
+        if ($total_hours == 0.0) {
+            $billable_pct = 0.0;
+        } else {
+            $billable_pct = round($total_billable_hours / $total_hours * 100.0);
+        }
+
+        $descr = _("Billable Hours") . ' (' . $billable_pct . '%)';
+        $result['footer'] = array();
+        $result['footer'][] = array(
+            'hours'       => sprintf('%.02f', $total_billable_hours),
+            'description' => $descr);
+
+        $descr = _("Non-billable Hours") . ' (' . (100.0 - $billable_pct) . '%)';
+        $result['footer'][] = array(
+            'hours'       => sprintf('%.02f', $total_hours - $total_billable_hours),
+            'description' => $descr);
+        $result['footer'][] = array(
+            'hours'       => sprintf('%.02f', $total_hours),
+            'description' => _("Total Hours"),
+            'approval'    => '<div id="approval">' . _("Approved By:") .
+                             ' ________________________________________ ' .
+                             '&nbsp;</div>');
+        break;
+    }
+
+    return $result;
+}
+
+function renderSubtotals(&$table_data, $hours, $billable_hours, $value)
+{
+    $billable_pct = ($hours == 0.0) ? 0.0 :
+         round($billable_hours / $hours * 100.0);
+    $descr = _("Billable Hours") . ' (' . $billable_pct . '%)';
+    $table_data[] = array(
+        'date'             => '',
+        'employee'         => '',
+        'client'           => '',
+        'billable'         => '',
+        'note'             => '',
+        '_type_name'       => '',
+        '_costobject_name' => '',
+        'hours'            => sprintf('%.02f', $billable_hours),
+        'description'      => $descr);
+     $descr = _("Non-billable Hours") . ' (' . (100.0 - $billable_pct) . '%)';
+     $table_data[] = array(
+        'hours'       => sprintf('%.02f', $hours - $billable_hours),
+        'description' => $descr);
+     $table_data[] = array(
+        'hours'       => sprintf('%.02f', $hours),
+        'description' => sprintf(_("Total Hours for %s"), $value),
+         );
+
+    return;
+}
+
+function _hermes_listCostObjects($criteria)
+{
+    require_once dirname(__FILE__) . '/base.php';
+
+    if (!$GLOBALS['conf']['time']['deliverables']) {
+        return array();
+    }
+
+    $deliverables = $GLOBALS['hermes']->listDeliverables($criteria);
+    if (is_a($deliverables, 'PEAR_Error')) {
+        return PEAR::raiseError(sprintf(_("An error occurred retrieving deliverables: %s"), $deliverables->getMessage()));
+    }
+
+    if (empty($criteria['id'])) {
+        /* Build heirarchical tree. */
+        $levels = array();
+        $hash = array();
+        foreach ($deliverables as $deliverable) {
+            if (empty($deliverable['parent'])) {
+                $parent = -1;
+            } else {
+                $parent = $deliverable['parent'];
+            }
+            $levels[$parent][$deliverable['id']] = $deliverable['name'];
+            $hash[$deliverable['id']] = $deliverable;
+        }
+
+        /* Sort levels alphabetically, keeping keys intact. */
+        foreach ($levels as $key => $level) {
+            asort($levels[$key]);
+        }
+
+        /* Traverse the tree and glue them back together. Lots of magic
+         * involved, so don't try to understand. */
+        $elts = array();
+        $stack = empty($levels[-1]) ? array() : array(-1);
+        while (count($stack)) {
+            if (!(list($key, $val) = each($levels[$stack[count($stack) - 1]]))) {
+                array_pop($stack);
+                continue;
+            }
+            $elts[$key] = str_repeat(' + ', count($stack)-1) . $val;
+            if (!empty($levels[$key])) {
+                $stack[] = $key;
+            }
+        }
+
+        $results = array();
+        foreach ($elts as $key => $value) {
+            $results[] = array('id'       => $key,
+                               'active'   => $hash[$key]['active'],
+                               'estimate' => $hash[$key]['estimate'],
+                               'name'     => $value);
+        }
+    } else {
+        $results = $deliverables;
+    }
+
+    if (!$results) {
+        return array();
+    }
+
+    return array(array('category' => _("Deliverables"),
+                       'objects'  => $results));
+}
+
+/**
+ * Retrieve list of job types.
+ *
+ * @abstract
+ *
+ * @param array $criteria  Hash of filter criteria:
+ *
+ *                      'enabled' => If present, only retrieve enabled
+ *                                   or disabled job types.
+ *
+ * @return mixed Associative array of job types, or PEAR_Error on failure.
+ */
+function _hermes_listJobTypes($criteria = array())
+{
+    require_once dirname(__FILE__) . '/base.php';
+    return $GLOBALS['hermes']->listJobTypes($criteria);
+}
+
+function _hermes_listClients()
+{
+    require_once dirname(__FILE__) . '/base.php';
+    return Hermes::listClients();
+}
+
+function _hermes_recordTime($date, $client, $jobType,
+                            $costObject, $hours, $billable = true,
+                            $description = '', $notes = '')
+{
+    require_once dirname(__FILE__) . '/base.php';
+    require_once 'Date.php';
+    require_once HERMES_BASE . '/lib/Forms/Time.php';
+
+    $dateobj = new Horde_Date($date);
+    $date['year'] = $dateobj->year;
+    $date['month'] = $dateobj->month;
+    $date['day'] = $dateobj->mday;
+
+    $vars = Horde_Variables::getDefaultVariables();
+    $vars->set('date', $date);
+    $vars->set('client', $client);
+    $vars->set('jobType', $jobType);
+    $vars->set('costObject', $costObject);
+    $vars->set('hours', $hours);
+    $vars->set('billable', $billable);
+    $vars->set('description', $description);
+    $vars->set('notes', $notes);
+
+    // Validate and submit the data
+    $form = new TimeEntryForm($vars);
+    $form->setSubmitted();
+    $form->useToken(false);
+    if ($form->validate($vars)) {
+        $form->getInfo($vars, $info);
+        $result = $GLOBALS['hermes']->enterTime(Horde_Auth::getAuth(), $info);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        } else {
+            return true;
+        }
+    } else {
+        return PEAR::raiseError(_("Invalid entry: check data and retry."));
+    }
+}
diff --git a/hermes/lib/base.php b/hermes/lib/base.php
new file mode 100644 (file)
index 0000000..013a458
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+/**
+ * $Horde: hermes/lib/base.php,v 1.40 2009/07/13 20:05:45 slusarz Exp $
+ *
+ * Copyright 2001-2007 Robert E. Coyle <robertecoyle@hotmail.com>
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ *
+ * Hermes base inclusion file.
+ *
+ * This file brings in all of the dependencies that every Hermes script
+ * will need, and sets up objects that all scripts use.
+ */
+
+// Check for a prior definition of HORDE_BASE (perhaps by an
+// auto_prepend_file definition for site customization).
+if (!defined('HORDE_BASE')) {
+    define('HORDE_BASE', dirname(__FILE__) . '/../..');
+}
+
+// Load the Horde Framework core, and set up inclusion paths.
+require_once HORDE_BASE . '/lib/core.php';
+
+// Registry.
+$registry = Horde_Registry::singleton();
+try {
+    $registry->pushApp('hermes', !defined('AUTH_HANDLER'));
+} catch (Horde_Exception $e) {
+    if ($e->getCode() == 'permission_denied') {
+        Horde::authenticationFailureRedirect();
+    }
+    Horde::fatal($e, __FILE__, __LINE__, false);
+}
+$conf = &$GLOBALS['conf'];
+define('HERMES_TEMPLATES', $registry->get('templates'));
+$print_link = null;
+
+// Notification system.
+$notification = &Horde_Notification::singleton();
+$notification->attach('status');
+
+// Find the base file path of Hermes.
+if (!defined('HERMES_BASE')) {
+    define('HERMES_BASE', dirname(__FILE__) . '/..');
+}
+
+// Hermes base libraries.
+require_once HERMES_BASE . '/lib/Hermes.php';
+$GLOBALS['hermes'] = &Hermes::getDriver();
+
+// Horde libraries.
+require_once 'Horde/Form.php';
+require_once 'Horde/Form/Renderer.php';
+require_once 'Horde/Template.php';
+
+// Start compression.
+Horde::compressOutput();
diff --git a/hermes/lib/version.php b/hermes/lib/version.php
new file mode 100644 (file)
index 0000000..0bd0620
--- /dev/null
@@ -0,0 +1 @@
+<?php define('HERMES_VERSION', 'H4 (2.0-cvs)') ?>
diff --git a/hermes/locale/de_DE/LC_MESSAGES/hermes.mo b/hermes/locale/de_DE/LC_MESSAGES/hermes.mo
new file mode 100644 (file)
index 0000000..623bc32
Binary files /dev/null and b/hermes/locale/de_DE/LC_MESSAGES/hermes.mo differ
diff --git a/hermes/locale/en_US/help.xml b/hermes/locale/en_US/help.xml
new file mode 100644 (file)
index 0000000..bddd95a
--- /dev/null
@@ -0,0 +1,40 @@
+<?xml version="1.0"?>
+<!-- $Horde: hermes/locale/en_US/help.xml,v 1.3 2008/06/29 23:13:02 jan Exp $ -->
+<help>
+  <entry id="hermes-job-types">
+    <title>Hermes Job Types</title>
+    <para>A job type in Hermes characterizes what kind of work has been done
+    in a time slice. The administrator has set a number of job types to choose
+    from.</para>
+  </entry>
+  <entry id="hermes-cost-objects">
+    <title>Hermes Cost Objects</title>
+    <para>Hermes draws possible cost objects from a variety of sources, for
+    example deliverable defined by the administator in Hermes, tasks from a
+    todo list manager, or tickets from a ticket-tracking system.</para>
+  </entry>
+  <entry id="hermes-time-format">
+    <title>Hermes Time Format</title>
+    <para>Hermes does not format its times as hours and minutes, rather the
+    time slices are formatted as decimal portions of an hour.</para>
+  </entry>
+  <entry id="hermes-submit-slices">
+    <title>Submitting Time Slices</title>
+    <para>Submitting time slices &quot;marks&quot; them as finshed. This is
+    also a sign for the administrator or bookkeeping department that they are
+    ready to be further processed. As long as you didn't submit your slices,
+    they are still considered due to be changed, but after submitting them you
+    can no longer change them.</para>
+    <heading>Submitting Certain Time Slices</heading>
+    <para>While this cannot be easily achieved from the default time slice
+    view, which shows all time slices for all clients, and the tabs simply
+    group the time slices differently, this can be accomplished by using the
+    search facility. Set the &quot;Submitted?&quot; field to &quot;No&quot;
+    and select any other criteria from the various from fields, then click
+    &quot;Search&quot;.</para>
+    <para>On submission of the search form in that way, you are presented with
+    a view just like the initial time slice view, but containing just the
+    slices with the picked criteria, with all checkboxes already checked, so
+    you can just click &quot;Submit Selected Time&quot; to do so.</para>
+  </entry>
+</help>
diff --git a/hermes/locale/es_ES/LC_MESSAGES/hermes.mo b/hermes/locale/es_ES/LC_MESSAGES/hermes.mo
new file mode 100644 (file)
index 0000000..f5c7706
Binary files /dev/null and b/hermes/locale/es_ES/LC_MESSAGES/hermes.mo differ
diff --git a/hermes/locale/es_ES/help.xml b/hermes/locale/es_ES/help.xml
new file mode 100644 (file)
index 0000000..0202ccb
--- /dev/null
@@ -0,0 +1,47 @@
+<?xml version="1.0"?>
+<!-- $Horde: hermes/locale/es_ES/help.xml,v 1.2 2008/06/30 08:35:01 jan Exp $ -->
+<help>
+  <entry id="hermes-job-types" state="unknown">
+    <title>Tipos de objetos de trabajo/facturables de Hermes</title>
+    <heading>Tipos de objetos de trabajo/facturables de Hermes</heading>
+    <para>Hermes introduce el concepto de tareas a las que pertenece un periodo de tiempo, as&#xED; como los periodos de tiempo correspondientes a un cliente, aunque  la terminolog&#xED;a utilizada es para una 'objeto de gastos', pero creo que &#xE9;sto puede resultar algo confuso y por ello esta entrada.</para>
+    <para>Hermes (as&#xED; parece) presenta posibles objetos de gastos desde m&#xFA;ltiples or&#xED;genes, uno de los cuales son las notas de Mnemo, por lo que si quiere tener una tarea a la que asignar periodos de tiempo, una forma f&#xE1;cil de hacerlo ser&#xED;a crear una nota en Mnemo y asignar los periodos al objeto de gasto representado por la nota.</para>
+    <para>Alguien pronto podr&#xED;a desear a&#xF1;adir informaci&#xF3;n sobre otros or&#xED;genes de objetos de gastos. Creo que las incidencias de Whups tambi&#xE9;n se pueden usar como objetos de gastos, pero no estoy seguro.</para>
+  <!-- English entry:
+<entry id="hermes-job-types">
+    <title>Hermes Job Types</title>
+    <para>A job type in Hermes characterizes what kind of work has been done
+    in a time slice. The administrator has set a number of job types to choose
+    from.</para>
+  </entry>--></entry>
+  <entry id="hermes-cost-objects" state="new">
+    <title>Hermes Cost Objects</title>
+    <para>Hermes draws possible cost objects from a variety of sources, for
+    example deliverable defined by the administator in Hermes, tasks from a
+    todo list manager, or tickets from a ticket-tracking system.</para>
+  </entry>
+  <entry id="hermes-time-format" state="new">
+    <title>Hermes Time Format</title>
+    <para>Hermes does not format its times as hours and minutes, rather the
+    time slices are formatted as decimal portions of an hour.</para>
+  </entry>
+  <entry id="hermes-submit-slices" state="new">
+    <title>Submitting Time Slices</title>
+    <para>Submitting time slices "marks" them as finshed. This is
+    also a sign for the administrator or bookkeeping department that they are
+    ready to be further processed. As long as you didn't submit your slices,
+    they are still considered due to be changed, but after submitting them you
+    can no longer change them.</para>
+    <heading>Submitting Certain Time Slices</heading>
+    <para>While this cannot be easily achieved from the default time slice
+    view, which shows all time slices for all clients, and the tabs simply
+    group the time slices differently, this can be accomplished by using the
+    search facility. Set the "Submitted?" field to "No"
+    and select any other criteria from the various from fields, then click
+    "Search".</para>
+    <para>On submission of the search form in that way, you are presented with
+    a view just like the initial time slice view, but containing just the
+    slices with the picked criteria, with all checkboxes already checked, so
+    you can just click "Submit Selected Time" to do so.</para>
+  </entry>
+</help>
diff --git a/hermes/locale/fi_FI/LC_MESSAGES/hermes.mo b/hermes/locale/fi_FI/LC_MESSAGES/hermes.mo
new file mode 100644 (file)
index 0000000..b20c968
Binary files /dev/null and b/hermes/locale/fi_FI/LC_MESSAGES/hermes.mo differ
diff --git a/hermes/locale/zh_TW/LC_MESSAGES/hermes.mo b/hermes/locale/zh_TW/LC_MESSAGES/hermes.mo
new file mode 100644 (file)
index 0000000..986e6b8
Binary files /dev/null and b/hermes/locale/zh_TW/LC_MESSAGES/hermes.mo differ
diff --git a/hermes/po/.cvsignore b/hermes/po/.cvsignore
new file mode 100644 (file)
index 0000000..fd8854c
--- /dev/null
@@ -0,0 +1 @@
+messages.po
diff --git a/hermes/po/README b/hermes/po/README
new file mode 100644 (file)
index 0000000..a985e94
--- /dev/null
@@ -0,0 +1 @@
+see horde/po/README
diff --git a/hermes/po/de_DE.po b/hermes/po/de_DE.po
new file mode 100644 (file)
index 0000000..d7846d5
--- /dev/null
@@ -0,0 +1,760 @@
+# German translations for Hermes.
+# Copyright 2002-2009 The Horde Project
+# This file is distributed under the same license as the Hermes package.
+# Jan Schneider <jan@horde.org>, 2002-2008.
+#
+# deliverables: Ergebnisse
+# time: Zeiteintrag
+# timeslice: Zeitabschnitt
+msgid ""
+msgstr ""
+"Project-Id-Version: Hermes 1.0-cvs\n"
+"Report-Msgid-Bugs-To: dev@lists.horde.org\n"
+"POT-Creation-Date: 2008-08-01 10:45+0200\n"
+"PO-Revision-Date: 2008-07-02 11:38+0200\n"
+"Last-Translator: Jan Schneider <jan@horde.org>\n"
+"Language-Team: German <i18n@lists.horde.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=ISO-8859-1\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: lib/Forms/Time.php:139
+#, php-format
+msgid " (%0.2f hours)"
+msgstr " (%0.2f Stunden)"
+
+#: lib/Forms/Time.php:141
+#, php-format
+msgid " (%d%%, %0.2f of %0.2f hours)"
+msgstr " (%d%%, %0.2f von %0.2f Stunden)"
+
+#: lib/Admin.php:54 lib/Forms/Search.php:102
+msgid " (DISABLED)"
+msgstr " (DEAKTIVIERT)"
+
+#: lib/api.php:149
+#, php-format
+msgid "\"%s\" is not a defined table."
+msgstr "\"%s\" ist nicht als Tabelle definiert."
+
+#: templates/deliverables/list.inc:4
+#, php-format
+msgid "%s Deliverables"
+msgstr "Ergebnisse für %s"
+
+#: lib/Forms/Search.php:84
+msgid "- - None - -"
+msgstr "- - Keiner - -"
+
+#: lib/Forms/Time.php:111
+msgid "--- No Cost Object ---"
+msgstr "--- Keine Kostenstelle ---"
+
+#: lib/Forms/Time.php:61
+msgid "--- Select A Client ---"
+msgstr "--- Kunden auswählen ---"
+
+#: lib/Data/iif.php:64
+msgid "; Notes: "
+msgstr "; Bemerkungen: "
+
+#: entry.php:74 lib/Driver/sql.php:142
+msgid "Access denied; user cannot modify this timeslice."
+msgstr ""
+"Zugriff verweigert; Benutzer kann diesen Zeitabschnitt nicht bearbeiten."
+
+#: lib/Forms/Deliverable.php:59
+msgid "Active?"
+msgstr "Aktiv?"
+
+#: admin.php:59 admin.php:221
+msgid "Add Job Type"
+msgstr "Arbeitsart hinzufügen"
+
+#: config/prefs.php.dist:21
+msgid ""
+"Add stop watch name and start and end time to the description of the time "
+"entry?"
+msgstr ""
+"Stoppuhr-Bezeichnung und Start- und Endezeit in die Beschreibung des "
+"Zeiteintrags übernehmen?"
+
+#: lib/Forms/Time.php:237 lib/Forms/Time.php:325
+msgid "Additional Notes"
+msgstr "Zusätzliche Bemerkungen"
+
+#: admin.php:35
+msgid "Administration"
+msgstr "Administration"
+
+#: lib/Forms/Time.php:57 lib/Forms/Search.php:81 lib/Forms/Deliverable.php:37
+#, php-format
+msgid "An error occurred listing clients: %s"
+msgstr "Beim Anzeigen der Kunden ist ein Fehler aufgetreten: %s"
+
+#: lib/Forms/Time.php:40 lib/Forms/Search.php:95
+#, php-format
+msgid "An error occurred listing job types: %s"
+msgstr "Beim Anzeigen der Arbeitsart ist ein Fehler aufgetreten: %s"
+
+#: lib/Hermes.php:176
+#, php-format
+msgid "An error occurred listing users: %s"
+msgstr "Beim Anzeigen der Benutzer ist ein Fehler aufgetreten: %s"
+
+#: lib/api.php:335
+#, php-format
+msgid "An error occurred retrieving deliverables: %s"
+msgstr "Beim Lesen der Ergebnisse ist ein Fehler aufgetreten: %s"
+
+#: lib/api.php:289
+msgid "Approved By:"
+msgstr "Bestätigt durch:"
+
+#: lib/api.php:106
+msgid "Bill?"
+msgstr "Rechnung?"
+
+#: lib/api.php:276 lib/api.php:302
+msgid "Billable Hours"
+msgstr "In Rechnung zu stellende Stunden"
+
+#: lib/Admin.php:30 lib/Admin.php:102 lib/Forms/Time.php:226
+#: lib/Forms/Time.php:321 lib/Forms/Search.php:71
+msgid "Billable?"
+msgstr "Rechnung?"
+
+#: lib/Hermes.php:230
+msgid "By Client"
+msgstr "Nach Kunde"
+
+#: lib/Hermes.php:232
+msgid "By Cost Object"
+msgstr "Nach Kostenstelle"
+
+#: lib/Hermes.php:228
+msgid "By Date"
+msgstr "Nach Datum"
+
+#: lib/Hermes.php:229
+msgid "By Employee"
+msgstr "Nach Mitarbeiter"
+
+#: lib/Hermes.php:231
+msgid "By Job Type"
+msgstr "Nach Arbeitsart"
+
+#: lib/Driver/sql.php:581
+msgid "Cannot delete deliverable; it has children."
+msgstr "Ergebnis kann nicht gelöscht werden; es existieren Unterergebnisse."
+
+#: lib/Driver/sql.php:596
+msgid "Cannot delete deliverable; there is time entered on it."
+msgstr "Ergebnis kann nicht gelöscht werden; es existieren Zeiteinträge dafür."
+
+#: invoicing.php:38 lib/Admin.php:187 lib/api.php:92 lib/Forms/Time.php:210
+#: lib/Forms/Time.php:311 lib/Forms/Deliverable.php:28
+msgid "Client"
+msgstr "Kunde"
+
+#: lib/Admin.php:160
+msgid "Client Name"
+msgstr "Kundenname"
+
+#: lib/Forms/Search.php:42
+msgid "Clients"
+msgstr "Kunden"
+
+#: invoicing.php:75
+msgid "Combine same clients in one invoice"
+msgstr "Denselben Kunden in einer Rechnung zusammenfassen"
+
+#: lib/Forms/Export.php:37
+msgid "Comma-Separated Variable (.csv)"
+msgstr "Kommagetrennte Werte (.csv)"
+
+#: invoicing.php:46 lib/api.php:98 lib/Forms/Time.php:219
+msgid "Cost Object"
+msgstr "Kostenstelle"
+
+#: lib/Forms/Search.php:51
+msgid "Cost Objects"
+msgstr "Kostenstellen"
+
+#: invoicing.php:70
+msgid "Create invoice"
+msgstr "Rechnung erstellen"
+
+#: invoicing.php:44 lib/api.php:83 lib/Forms/Time.php:206
+#: lib/Forms/Time.php:307
+msgid "Date"
+msgstr "Datum"
+
+#: lib/Forms/Time.php:183
+msgid "Delete"
+msgstr "Löschen"
+
+#: lib/Forms/Time.php:183
+msgid "Delete Entry"
+msgstr "Eintrag löschen"
+
+#: admin.php:88 admin.php:220
+msgid "Delete Job Type"
+msgstr "Arbeitsart löschen"
+
+#: admin.php:93 admin.php:208
+msgid "Delete Job Type: Confirmation"
+msgstr "Arbeitsart löschen: Bestätigung"
+
+#: deliverables.php:107
+msgid "Delete This Deliverable"
+msgstr "Dieses Ergebnis löschen"
+
+#: scripts/purge.php:26
+#, php-format
+msgid "Deleting data that was exported/billed more than %s days ago.\n"
+msgstr ""
+"Lösche Daten, die vor mehr als %s Tagen exportiert/in Rechnung gestellt "
+"wurden.\n"
+
+#: lib/Driver.php:85
+#, php-format
+msgid "Deliverable %d not found."
+msgstr "Ergebnis %d nicht gefunden."
+
+#: lib/Forms/Deliverable.php:52
+msgid "Deliverable Detail"
+msgstr "Ergebnisdetail"
+
+#: deliverables.php:42
+msgid "Deliverable saved successfully."
+msgstr "Ergebnis erfolgreich gespeichert."
+
+#: deliverables.php:55
+msgid "Deliverable successfully deleted."
+msgstr "Ergebnis erfolgreich gelöscht."
+
+#: deliverables.php:61 lib/api.php:54 lib/api.php:387
+msgid "Deliverables"
+msgstr "Ergebnisse"
+
+#: invoicing.php:45 lib/api.php:112 lib/Forms/Time.php:235
+#: lib/Forms/Time.php:324 lib/Forms/Deliverable.php:61
+msgid "Description"
+msgstr "Beschreibung"
+
+#: lib/Forms/Deliverable.php:58
+msgid "Display Name"
+msgstr "Anzeigename"
+
+#: lib/Forms/Search.php:58
+msgid "Do not include entries after"
+msgstr "Keine Einträge einbeziehen nach"
+
+#: lib/Forms/Search.php:55
+msgid "Do not include entries before"
+msgstr "Keine Einträge einbeziehen vor"
+
+#: deliverables.php:103
+#, php-format
+msgid "Edit %s"
+msgstr "%s Bearbeiten"
+
+#: admin.php:130 admin.php:222
+msgid "Edit Client Settings"
+msgstr "Kundeneinstellungen bearbeiten"
+
+#: admin.php:122 admin.php:181
+msgid "Edit Client Settings, Step 2"
+msgstr "Kundeneinstellungen bearbeiten, Schritt 2"
+
+#: lib/Forms/Deliverable.php:30
+msgid "Edit Deliverables"
+msgstr "Ergebnisse bearbeiten"
+
+#: lib/api.php:236 lib/api.php:237
+msgid "Edit Entry"
+msgstr "Eintrag bearbeiten"
+
+#: admin.php:75 admin.php:220
+msgid "Edit Job Type"
+msgstr "Arbeitsart bearbeiten"
+
+#: admin.php:80
+msgid "Edit Job Type, Step 2"
+msgstr "Arbeitsart bearbeiten, Schritt 2"
+
+#: entry.php:94
+msgid "Edit Time"
+msgstr "Zeiteintrag bearbeiten"
+
+#: admin.php:103
+msgid "Edit job type"
+msgstr "Arbeitsart bearbeiten"
+
+#: admin.php:156
+msgid "Edit job type, Step 2"
+msgstr "Arbeitsart bearbeiten, Schritt 2"
+
+#: invoicing.php:39 lib/api.php:88 lib/Forms/Time.php:305
+msgid "Employee"
+msgstr "Mitarbeiter"
+
+#: lib/Forms/Search.php:36
+msgid "Employees"
+msgstr "Mitarbeiter"
+
+#: lib/Admin.php:28 lib/Admin.php:100
+msgid "Enabled?"
+msgstr "Aktiviert?"
+
+#: lib/Block/tree_menu.php:3 lib/Block/tree_menu.php:21
+msgid "Enter Time"
+msgstr "Zeit eintragen"
+
+#: deliverables.php:52
+#, php-format
+msgid "Error deleting deliverable: %s"
+msgstr "Fehler beim Löschen des Ergebnisses: %s"
+
+#: lib/Forms/Time.php:93 lib/Forms/Search.php:130
+#, php-format
+msgid "Error retrieving cost objects from \"%s\": %s"
+msgstr "Fehler beim Lesen der Kostenstellen aus \"%s\": %s"
+
+#: deliverables.php:38
+#, php-format
+msgid "Error saving deliverable: %s"
+msgstr "Fehler beim Speichern des Ergebnisses: %s"
+
+#: lib/Driver/sql.php:298
+#, php-format
+msgid "Error: %s"
+msgstr "Fehler: %s"
+
+#: lib/Forms/Deliverable.php:60
+msgid "Estimated Hours"
+msgstr "Geschätzte Stunden"
+
+#: lib/Forms/Export.php:56
+msgid "Export"
+msgstr "Exportieren"
+
+#: lib/Forms/Export.php:35
+msgid "Export Search Results"
+msgstr "Suchergebnisse exportieren"
+
+#: lib/Forms/Search.php:68
+msgid "Exported?"
+msgstr "Exportiert?"
+
+#: config/prefs.php.dist:9
+msgid "General Options"
+msgstr "Allgemeine Einstellungen"
+
+#: lib/Admin.php:32 lib/Admin.php:104
+msgid "Hourly Rate"
+msgstr "Stundensatz"
+
+#: invoicing.php:42 lib/api.php:100 lib/Forms/Time.php:222
+#: lib/Forms/Time.php:317
+msgid "Hours"
+msgstr "Stunden"
+
+#: lib/Admin.php:195
+msgid ""
+"ID for this client when exporting data, if different from the name displayed "
+"above."
+msgstr ""
+"Kunden-ID für das Exportieren, falls sie sich von dem oben angezeigten Namen "
+"unterscheidet."
+
+#: lib/api.php:452
+msgid "Invalid entry: check data and retry."
+msgstr ""
+"Ungültiger Eintrag: Überprüfen Sie die Daten und versuchen Sie es noch "
+"einmal."
+
+#: invoicing.php:122
+#, php-format
+msgid "Invoice for client %s successfuly created."
+msgstr "Die Rechnung für Kunde %s wurde erfolgreich erstellt."
+
+#: lib/api.php:56
+msgid "Invoicing"
+msgstr "Rechnungswesen"
+
+#: invoicing.php:32
+msgid "Invoicing system is not installed."
+msgstr "Rechnungssystem ist nicht installiert."
+
+#: invoicing.php:40 lib/Admin.php:27 lib/Admin.php:95 lib/Admin.php:129
+#: lib/api.php:96 lib/Forms/Time.php:216 lib/Forms/Time.php:316
+msgid "Job Type"
+msgstr "Arbeitsart"
+
+#: lib/Forms/Search.php:48
+msgid "Job Types"
+msgstr "Arbeitsarten"
+
+#: lib/Admin.php:67
+msgid "JobType Name"
+msgstr "Arbeitsart-Bezeichnung"
+
+#: lib/Forms/Export.php:50
+msgid "Mark the time as exported?"
+msgstr "Diese Zeitspanne als exportiert markieren?"
+
+#: lib/Forms/Export.php:38
+msgid "Microsoft Excel (.xls)"
+msgstr "Microsoft Excel (.xls)"
+
+#: time.php:72
+msgid "My Time"
+msgstr "Meine Zeit"
+
+#: time.php:62
+msgid "My Unsubmitted Time"
+msgstr "Meine nicht abgeschickten Zeiteinträge"
+
+#: lib/Hermes.php:63
+msgid "My _Time"
+msgstr "_Meine Zeit"
+
+#: deliverables.php:105
+msgid "New Sub-deliverable"
+msgstr "Neues Unterergebnis"
+
+#: entry.php:94 lib/Forms/Time.php:186
+msgid "New Time"
+msgstr "Neuer Zeiteintrag"
+
+#: templates/deliverables/list.inc:10
+msgid "New Top-level Deliverable"
+msgstr "Neues Hauptergebnis"
+
+#: invoicing.php:74 lib/Admin.php:125 lib/api.php:71 lib/Forms/Time.php:225
+#: lib/Forms/Time.php:320 lib/Forms/Search.php:64 lib/Forms/Export.php:49
+msgid "No"
+msgstr "Nein"
+
+#: lib/Driver.php:28
+#, php-format
+msgid "No job type with ID \"%s\"."
+msgstr "Keine Arbeitsart mit der ID \"%s\"."
+
+#: search.php:54 search.php:60
+msgid "No search to export!"
+msgstr "Keine Suche zu exportieren!"
+
+#: search.php:70
+msgid "No time to export!"
+msgstr "Keine Zeiteinträge zu exportieren!"
+
+#: time.php:38
+msgid "No timeslices were selected to submit."
+msgstr "Es wurden keine Zeitabschnitte ausgewählt."
+
+#: lib/api.php:282 lib/api.php:313
+msgid "Non-billable Hours"
+msgstr "Nicht in Rechnung zu stellende Stunden"
+
+#: lib/Hermes.php:217
+msgid "Not found."
+msgstr "Nicht gefunden."
+
+#: lib/Driver.php:49 lib/Driver.php:66 lib/Driver.php:115 lib/Driver.php:133
+#: lib/Driver.php:146
+msgid "Not implemented."
+msgstr "Nicht implementiert."
+
+#: lib/api.php:114
+msgid "Notes"
+msgstr "Bemerkungen"
+
+#: lib/Forms/Export.php:39
+msgid "QuickBooks (.iif)"
+msgstr "QuickBooks (.iif)"
+
+#: invoicing.php:41
+msgid "Rate"
+msgstr "Rate"
+
+#: lib/Admin.php:132
+msgid "Really delete this job type? This may cause data problems!!"
+msgstr ""
+"Diese Arbeitsart wirklich löschen? Das kann zu Problemen mit den Daten "
+"führen!"
+
+#: lib/Forms/Time.php:188
+msgid "Save"
+msgstr "Speichern"
+
+#: lib/Forms/Search.php:74
+msgid "Search"
+msgstr "Suche"
+
+#: lib/Forms/Search.php:32
+msgid "Search For Time"
+msgstr "Nach Zeit suchen"
+
+#: search.php:123 lib/api.php:138
+msgid "Search Results"
+msgstr "Suchergebnisse"
+
+#: lib/Block/tree_menu.php:29
+msgid "Search Time"
+msgstr "Zeit suchen"
+
+#: search.php:92
+msgid "Search for Time"
+msgstr "Nach Zeit suchen"
+
+#: lib/Admin.php:190 lib/Forms/Time.php:232 lib/Forms/Time.php:277
+msgid "See Attached Timesheet"
+msgstr "Siehe angehängte Zeitübersicht"
+
+#: lib/Forms/Deliverable.php:22
+msgid "Select Client"
+msgstr "Kunde auswählen"
+
+#: invoicing.php:76
+msgid "Select hours to be invoiced"
+msgstr ""
+"Wählen Sie die Zeiteinträge aus, die in Rechnung gestellt werden sollen"
+
+#: lib/Forms/Export.php:43
+msgid "Select the export format"
+msgstr "Wählen Sie das Exportformat"
+
+#: config/prefs.php.dist:11
+msgid "Set preferences on the stop watch timer."
+msgstr "Legen Sie Ihre Einstellungen für die Stoppuhr fest."
+
+#: lib/Admin.php:190
+#, php-format
+msgid ""
+"Should users enter descriptions of their timeslices for this client? If not, "
+"the description will automatically be \"%s\"."
+msgstr ""
+"Müssen die Benutzer Kommentare für Zeitabschnitte bei diesem Kunden angeben? "
+"Wenn nicht, wird automatisch der Kommentar \"%s\" eingefügt."
+
+#: lib/Block/tree_stopwatch.php:28
+msgid "Start Watch"
+msgstr "Stoppuhr starten"
+
+#: start.php:20 start.php:44 lib/Block/tree_stopwatch.php:3
+msgid "Stop Watch"
+msgstr "Stoppuhr"
+
+#: start.php:21
+msgid "Stop watch description"
+msgstr "Bezeichnung für Stoppuhr"
+
+#: templates/time/form.html:5
+msgid "Submit Selected Time"
+msgstr "Ausgewählte Zeiteinträge abschicken"
+
+#: lib/Forms/Search.php:65
+msgid "Submitted?"
+msgstr "Abgeschickt?"
+
+#: lib/Hermes.php:227
+msgid "Summary"
+msgstr "Zusammenfassung"
+
+#: lib/Forms/Export.php:40
+msgid "Tab-Separated Variable (.tsv, .txt)"
+msgstr "Tabgetrennte Werte (.tsv, .txt)"
+
+#: lib/Table.php:138
+msgid "Table"
+msgstr "Tabelle"
+
+#: admin.php:175
+msgid "The client settings have been modified."
+msgstr "Die Kundeneinstellungen wurde geändert."
+
+#: admin.php:51
+#, php-format
+msgid "The job type \"%s\" has been added."
+msgstr "Die Arbeitsart \"%s\" wurde hinzugefügt."
+
+#: admin.php:197
+msgid "The job type has been deleted."
+msgstr "Die Arbeitsart wurde gelöscht."
+
+#: admin.php:148
+msgid "The job type has been modified."
+msgstr "Die Arbeitsart wurde geändert."
+
+#: admin.php:202
+msgid "The job type was not deleted."
+msgstr "Die Arbeitsart wurde nicht gelöscht."
+
+#: start.php:40
+#, php-format
+msgid ""
+"The stop watch \"%s\" has been started and will appear in the sidebar at the "
+"next refresh."
+msgstr ""
+"Die Stoppuhr \"%s\" wurde gestartet und erscheint nach der nächsten "
+"Aktualisierung im linken Menü."
+
+#: entry.php:31
+#, php-format
+msgid "The stop watch \"%s\" has been stopped."
+msgstr "Die Stoppuhr \"%s\" wurde angehalten."
+
+#: search.php:32 time.php:28
+msgid "The time entry was successfully deleted."
+msgstr "Der Zeiteintrag wurde erfolgreich gelöscht."
+
+#: lib/Admin.php:157
+msgid "There are no clients to edit"
+msgstr "Es gibt keine Kunden, die bearbeitet werden können"
+
+#: lib/Forms/Time.php:65 lib/Forms/Deliverable.php:42
+msgid "There are no clients which you have access to."
+msgstr "Sie haben auf keinen Kunden Zugriff."
+
+#: lib/Forms/Time.php:49
+msgid "There are no job types configured."
+msgstr "Es wurden keine Arbeitsarten konfiguriert."
+
+#: lib/Admin.php:64
+msgid "There are no job types to edit"
+msgstr "Es gibt keine Arbeitsarten, die bearbeitet werden können"
+
+#: invoicing.php:28
+msgid "There is no submitted billable hours."
+msgstr "Es gibt keine abgeschickten Zeiteinträge."
+
+#: admin.php:53
+#, php-format
+msgid "There was an error adding the job type: %s."
+msgstr "Beim Hinzufügen der Arbeitsart ist ein Fehler aufgetreten: %s."
+
+#: admin.php:199
+#, php-format
+msgid "There was an error deleting the job type: %s."
+msgstr "Beim Löschen der Arbeisart ist ein Fehler aufgetreten: %s."
+
+#: search.php:30 time.php:26
+#, php-format
+msgid "There was an error deleting the time: %s"
+msgstr "Beim Löschen des Zeiteintrags ist ein Fehler aufgetreten: %s"
+
+#: admin.php:173
+#, php-format
+msgid "There was an error editing the client settings: %s."
+msgstr ""
+"Beim Bearbeiten der Kundeneinstellungen ist ein Fehler aufgetreten: %s."
+
+#: admin.php:150
+#, php-format
+msgid "There was an error editing the job type: %s."
+msgstr "Beim Bearbeiten der Arbeitsart ist ein Fehler aufgetreten: %s."
+
+#: entry.php:53
+#, php-format
+msgid "There was an error storing your timesheet: %s"
+msgstr "Beim Speichern Ihrer Zeiteinträge is ein Fehler aufgetreten: %s"
+
+#: time.php:46
+#, php-format
+msgid "There was an error submitting your time: %s"
+msgstr "Beim Abschicken Ihres Zeiteintrags ist ein Fehler aufgetreten: %s"
+
+#: lib/Admin.php:180
+msgid "This is not a valid client."
+msgstr "Dies ist kein gültiger Kunde."
+
+#: lib/Admin.php:87
+msgid "This is not a valid job type."
+msgstr "Dies ist keine gültige Arbeitsart."
+
+#: lib/api.php:52
+msgid "Time Review Screen"
+msgstr "Überprüfung der Zeiteinträge"
+
+#: config/prefs.php.dist:10
+msgid "Timer Options"
+msgstr "Stoppuhr-Einstellungen"
+
+#: invoicing.php:43
+msgid "Total"
+msgstr "Insgesamt"
+
+#: lib/api.php:288
+msgid "Total Hours"
+msgstr "Stunden gesamt"
+
+#: lib/api.php:319
+#, php-format
+msgid "Total Hours for %s"
+msgstr "Stunden gesamt für %s"
+
+#: lib/Forms/Time.php:267
+msgid "Update Submitted Time"
+msgstr "Abgeschickten Zeiteintrag aktualisieren"
+
+#: lib/Forms/Time.php:182
+msgid "Update Time"
+msgstr "Zeiteintrag aktualisieren"
+
+#: lib/Forms/Time.php:268
+msgid "Update time"
+msgstr "Zeiteintrag aktualisieren"
+
+#: entry.php:29
+#, php-format
+msgid "Using the \"%s\" stop watch from %s to %s"
+msgstr "Mit Stoppuhr \"%s\" von %s bis %s"
+
+#: invoicing.php:74 lib/Admin.php:125 lib/api.php:70 lib/Forms/Time.php:225
+#: lib/Forms/Time.php:320 lib/Forms/Search.php:63 lib/Forms/Export.php:48
+msgid "Yes"
+msgstr "Ja"
+
+#: entry.php:47
+msgid "Your time was successfully entered."
+msgstr "Ihr Zeiteintrag wurde erfolgreich gespeichert."
+
+#: time.php:48
+msgid "Your time was successfully submitted."
+msgstr "Ihr Zeiteintrag wurde erfolgreich abgeschickt."
+
+#: entry.php:43
+msgid "Your time was successfully updated."
+msgstr "Ihr Zeiteintrag wurde erfolgreich aktualisiert."
+
+#: lib/Hermes.php:82
+msgid "_Admin"
+msgstr "_Administration"
+
+#: lib/Hermes.php:68
+msgid "_Deliverables"
+msgstr "E_rgebnisse"
+
+#: lib/Hermes.php:72
+msgid "_Invoicing"
+msgstr "_Rechnungswesen"
+
+#: lib/Hermes.php:64
+msgid "_New Time"
+msgstr "_Neuer Zeiteintrag"
+
+#: lib/Hermes.php:77
+msgid "_Print"
+msgstr "_Drucken"
+
+#: lib/Hermes.php:65
+msgid "_Search"
+msgstr "_Suche"
+
+#: lib/api.php:194
+msgid "no client"
+msgstr "Kein Kunde"
diff --git a/hermes/po/es_ES.po b/hermes/po/es_ES.po
new file mode 100644 (file)
index 0000000..69eaf92
--- /dev/null
@@ -0,0 +1,747 @@
+# Spanish translations for hermes package
+# Traducciones al español para el paquete hermes.
+# Copyright 2008-2009 The Horde Project
+# This file is distributed under the same license as the hermes package.
+# Automatically generated, 2008.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Hermes 1.0-cvs\n"
+"Report-Msgid-Bugs-To: dev@lists.horde.org\n"
+"POT-Creation-Date: 2008-03-16 13:58+0100\n"
+"PO-Revision-Date: 2008-03-16 13:58+0100\n"
+"Last-Translator: Manuel P. Ayala <mayala@unex.es>\n"
+"Language-Team: i18n@lists.horde.org\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=ISO-8859-1\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: lib/Forms/Time.php:137
+#, php-format
+msgid " (%0.2f hours)"
+msgstr " (%0.2f horas)"
+
+#: lib/Forms/Time.php:139
+#, php-format
+msgid " (%d%%, %0.2f of %0.2f hours)"
+msgstr " (%d%%, %0.2f de %0.2f horas)"
+
+#: lib/Admin.php:54 lib/Forms/Search.php:102
+msgid " (DISABLED)"
+msgstr " (DESACTIVADO)"
+
+#: lib/api.php:151
+#, php-format
+msgid "\"%s\" is not a defined table."
+msgstr "\"%s\" no es una tabla definida."
+
+#: templates/deliverables/list.inc:4
+#, php-format
+msgid "%s Deliverables"
+msgstr "%s prestaciones"
+
+#: lib/Forms/Search.php:84
+msgid "- - None - -"
+msgstr "- - Ninguno - -"
+
+#: lib/Forms/Time.php:109
+msgid "--- No Cost Object ---"
+msgstr "--- Sin objetos de gastos ---"
+
+#: lib/Forms/Time.php:60
+msgid "--- Select A Client ---"
+msgstr "--- Seleccione un cliente ---"
+
+#: lib/Data/iif.php:64
+msgid "; Notes: "
+msgstr "; Observaciones: "
+
+#: lib/Driver/sql.php:142
+msgid "Access denied; user cannot modify this timeslice."
+msgstr "Se ha denegado el acceso: el usuario no puede modificar este periodo."
+
+#: lib/Forms/Deliverable.php:59
+msgid "Active?"
+msgstr "¿Activa?"
+
+#: admin.php:59 admin.php:221
+msgid "Add Job Type"
+msgstr "Añadir tipo de trabajo"
+
+#: config/.bak/prefs.php.dist:21
+msgid "Add stop watch name to the description of the time entry?"
+msgstr "¿Añadir nombre del cronómetro a la descripción del registro de tiempo?"
+
+#: lib/Forms/Time.php:224 lib/Forms/Time.php:306
+msgid "Additional Notes"
+msgstr "Observaciones adicionales"
+
+#: admin.php:35
+msgid "Administration"
+msgstr "Administración"
+
+#: lib/Forms/Time.php:57 lib/Forms/Search.php:81 lib/Forms/Deliverable.php:37
+#, php-format
+msgid "An error occurred listing clients: %s"
+msgstr "Se ha producido un error al listar los clientes: %s"
+
+#: lib/Forms/Time.php:40 lib/Forms/Search.php:95
+#, php-format
+msgid "An error occurred listing job types: %s"
+msgstr "Se ha producido un error al listar los tipos de trabajos: %s"
+
+#: lib/Hermes.php:176
+#, php-format
+msgid "An error occurred listing users: %s"
+msgstr "Se ha producido un error al listar los usuarios: %s"
+
+#: lib/api.php:345
+#, php-format
+msgid "An error occurred retrieving deliverables: %s"
+msgstr "Se ha producido un error al recuperar los disponibles: %s"
+
+#: lib/api.php:298
+msgid "Approved By:"
+msgstr "Aprobado por:"
+
+#: lib/api.php:108
+msgid "Bill?"
+msgstr "¿Facturar?"
+
+#: lib/api.php:285 lib/api.php:311
+msgid "Billable Hours"
+msgstr "Horas facturables"
+
+#: lib/Admin.php:30 lib/Admin.php:102 lib/Forms/Time.php:213
+#: lib/Forms/Time.php:302 lib/Forms/Search.php:71
+msgid "Billable?"
+msgstr "¿Facturables?"
+
+#: lib/Hermes.php:230
+msgid "By Client"
+msgstr "Por clientes"
+
+#: lib/Hermes.php:232
+msgid "By Cost Object"
+msgstr "Por objetos de gastos"
+
+#: lib/Hermes.php:228
+msgid "By Date"
+msgstr "Por fechas"
+
+#: lib/Hermes.php:229
+msgid "By Employee"
+msgstr "Por empleados"
+
+#: lib/Hermes.php:231
+msgid "By Job Type"
+msgstr "Por tipos de trabajos"
+
+#: lib/Driver/sql.php:578
+msgid "Cannot delete deliverable; it has children."
+msgstr "No se puede eliminar la prestación; tiene descendientes."
+
+#: lib/Driver/sql.php:593
+msgid "Cannot delete deliverable; there is time entered on it."
+msgstr "No se puede eliminar la prestación; se han introducido horas."
+
+#: invoicing.php:38 lib/api.php:94 lib/Admin.php:187 lib/Forms/Time.php:197
+#: lib/Forms/Time.php:292 lib/Forms/Deliverable.php:28
+msgid "Client"
+msgstr "Cliente"
+
+#: lib/Admin.php:160
+msgid "Client Name"
+msgstr "Nombre del cliente"
+
+#: lib/Forms/Search.php:42
+msgid "Clients"
+msgstr "Clientes"
+
+#: invoicing.php:75
+msgid "Combine same clients in one invoice"
+msgstr "Combinar los mismos clientes en una única factura"
+
+#: lib/Forms/Export.php:37
+msgid "Comma-Separated Variable (.csv)"
+msgstr "Variables separadas por comas (.csv)"
+
+#: invoicing.php:46 lib/api.php:100 lib/Forms/Time.php:206
+msgid "Cost Object"
+msgstr "Objeto de gastos"
+
+#: lib/Forms/Search.php:52
+msgid "Cost Objects"
+msgstr "Objetos de gastos"
+
+#: invoicing.php:70
+msgid "Create invoice"
+msgstr "Crear factura"
+
+#: invoicing.php:44 lib/api.php:85 lib/Forms/Time.php:193
+#: lib/Forms/Time.php:288
+msgid "Date"
+msgstr "Fecha"
+
+#: lib/Forms/Time.php:174
+msgid "Delete"
+msgstr "Eliminar"
+
+#: lib/Forms/Time.php:174
+msgid "Delete Entry"
+msgstr "Eliminar entrada"
+
+#: admin.php:88 admin.php:220
+msgid "Delete Job Type"
+msgstr "Eliminar tipo de trabajo"
+
+#: admin.php:93 admin.php:208
+msgid "Delete Job Type: Confirmation"
+msgstr "Eliminar tipo de trabajo: confirmación"
+
+#: deliverables.php:107
+msgid "Delete This Deliverable"
+msgstr "Eliminar esta prestación"
+
+#: scripts/purge.php:26
+#, php-format
+msgid "Deleting data that was exported/billed more than %s days ago.\n"
+msgstr "Eliminando datos exportados/facturados hace más de %s días.\n"
+
+#: lib/Driver.php:85
+#, php-format
+msgid "Deliverable %d not found."
+msgstr "No se encontró la prestación %d."
+
+#: lib/Forms/Deliverable.php:52
+msgid "Deliverable Detail"
+msgstr "Detalles de la prestación"
+
+#: deliverables.php:42
+msgid "Deliverable saved successfully."
+msgstr "Se ha guardado correctamente la prestación."
+
+#: deliverables.php:55
+msgid "Deliverable successfully deleted."
+msgstr "Se ha eliminado correctamente la prestación."
+
+#: deliverables.php:61 lib/api.php:54 lib/api.php:397
+msgid "Deliverables"
+msgstr "Prestaciones"
+
+#: invoicing.php:45 lib/api.php:114 lib/Forms/Time.php:222
+#: lib/Forms/Time.php:305 lib/Forms/Deliverable.php:61
+msgid "Description"
+msgstr "Descripción"
+
+#: lib/Forms/Deliverable.php:58
+msgid "Display Name"
+msgstr "Mostrar nombre"
+
+#: lib/Forms/Search.php:58
+msgid "Do not include entries after"
+msgstr "Omitir posteriores a"
+
+#: lib/Forms/Search.php:55
+msgid "Do not include entries before"
+msgstr "Omitir anteriores a"
+
+#: deliverables.php:103
+#, php-format
+msgid "Edit %s"
+msgstr "Modificar %s"
+
+#: admin.php:130 admin.php:222
+msgid "Edit Client Settings"
+msgstr "Modificar opciones del cliente"
+
+#: admin.php:122 admin.php:181
+msgid "Edit Client Settings, Step 2"
+msgstr "Modificar opciones del cliente. Paso 2"
+
+#: lib/Forms/Deliverable.php:30
+msgid "Edit Deliverables"
+msgstr "Modificar prestaciones"
+
+#: lib/api.php:245 lib/api.php:246
+msgid "Edit Entry"
+msgstr "Modificar entrada"
+
+#: admin.php:75 admin.php:220
+msgid "Edit Job Type"
+msgstr "Modificar tipo de trabajo"
+
+#: admin.php:80
+msgid "Edit Job Type, Step 2"
+msgstr "Modificar tipo de trabajo. Paso 2"
+
+#: entry.php:84
+msgid "Edit Time"
+msgstr "Modificar hora"
+
+#: admin.php:103
+msgid "Edit job type"
+msgstr "Modificar tipo de trabajo"
+
+#: admin.php:156
+msgid "Edit job type, Step 2"
+msgstr "Modificar tipo de trabajo. Paso 2"
+
+#: invoicing.php:39 lib/api.php:90 lib/Forms/Time.php:286
+msgid "Employee"
+msgstr "Empleado"
+
+#: lib/Forms/Search.php:36
+msgid "Employees"
+msgstr "Empleados"
+
+#: lib/Admin.php:28 lib/Admin.php:100
+msgid "Enabled?"
+msgstr "¿Activado?"
+
+#: lib/Block/tree_menu.php:3 lib/Block/tree_menu.php:21
+msgid "Enter Time"
+msgstr "Añadir"
+
+#: deliverables.php:52
+#, php-format
+msgid "Error deleting deliverable: %s"
+msgstr "Se produjo un error al eliminar la prestación: %s"
+
+#: lib/Forms/Time.php:91 lib/Forms/Search.php:130
+#, php-format
+msgid "Error retrieving cost objects from \"%s\": %s"
+msgstr "Error al recuperar objetos de gastos de \"%s\": %s"
+
+#: deliverables.php:38
+#, php-format
+msgid "Error saving deliverable: %s"
+msgstr "Error al guardar la prestación: %s"
+
+#: lib/Driver/sql.php:295
+#, php-format
+msgid "Error: %s"
+msgstr "Error: %s"
+
+#: lib/Forms/Deliverable.php:60
+msgid "Estimated Hours"
+msgstr "Horas estimadas"
+
+#: lib/Forms/Export.php:56
+msgid "Export"
+msgstr "Exportar"
+
+#: lib/Forms/Export.php:35
+msgid "Export Search Results"
+msgstr "Resultados de la búsqueda"
+
+#: lib/Forms/Search.php:68
+msgid "Exported?"
+msgstr "¿Exportadas?"
+
+#: config/.bak/prefs.php.dist:9
+msgid "General Options"
+msgstr "Opciones generales"
+
+#: lib/Admin.php:32 lib/Admin.php:104
+msgid "Hourly Rate"
+msgstr "Tasa horaria"
+
+#: invoicing.php:42 lib/api.php:102 lib/Forms/Time.php:209
+#: lib/Forms/Time.php:298
+msgid "Hours"
+msgstr "Horas"
+
+#: lib/Admin.php:195
+msgid ""
+"ID for this client when exporting data, if different from the name displayed "
+"above."
+msgstr ""
+"ID del cliente al exportar datos, si difiere del nombre mostrado "
+"anteriormente."
+
+#: lib/api.php:471
+msgid "Invalid entry: check data and retry."
+msgstr "Entrada no válida: compruebe los datos y vuelva a intentarlo."
+
+#: invoicing.php:122
+#, php-format
+msgid "Invoice for client %s successfuly created."
+msgstr "Se creó correctamente la factura del cliente %s."
+
+#: lib/api.php:56
+msgid "Invoicing"
+msgstr "Facturación"
+
+#: invoicing.php:32
+msgid "Invoicing system is not installed."
+msgstr "EL sistema de facturación no está instalado."
+
+#: invoicing.php:40 lib/api.php:98 lib/Admin.php:27 lib/Admin.php:95
+#: lib/Admin.php:129 lib/Forms/Time.php:203 lib/Forms/Time.php:297
+msgid "Job Type"
+msgstr "Tipo de trabajo"
+
+#: lib/Forms/Search.php:48
+msgid "Job Types"
+msgstr "Tipos de trabajos"
+
+#: lib/Admin.php:67
+msgid "JobType Name"
+msgstr "Tipo de trabajo"
+
+#: lib/Forms/Export.php:50
+msgid "Mark the time as exported?"
+msgstr "¿Señalar la hora como exportada?"
+
+#: lib/Forms/Export.php:38
+msgid "Microsoft Excel (.xls)"
+msgstr "Excel de Microsoft (.xls)"
+
+#: time.php:72
+msgid "My Time"
+msgstr "Mis horas"
+
+#: time.php:62
+msgid "My Unsubmitted Time"
+msgstr "Mis horas no enviadas"
+
+#: lib/Hermes.php:63
+msgid "My _Time"
+msgstr "Mis _horas"
+
+#: deliverables.php:105
+msgid "New Sub-deliverable"
+msgstr "Añadir subprestación"
+
+#: entry.php:84 lib/Forms/Time.php:177
+msgid "New Time"
+msgstr "Añadir"
+
+#: templates/deliverables/list.inc:10
+msgid "New Top-level Deliverable"
+msgstr "Añadir prestación principal"
+
+#: invoicing.php:74 lib/api.php:73 lib/Admin.php:125 lib/Forms/Time.php:212
+#: lib/Forms/Time.php:301 lib/Forms/Export.php:49 lib/Forms/Search.php:64
+msgid "No"
+msgstr "No"
+
+#: lib/Driver.php:28
+#, php-format
+msgid "No job type with ID \"%s\"."
+msgstr "No hay ningún tipo de trabajo con el ID \"%s\"."
+
+#: search.php:54 search.php:60
+msgid "No search to export!"
+msgstr "¡No hay búsquedas a exportar!"
+
+#: search.php:70
+msgid "No time to export!"
+msgstr "¡No hay horas a exportar!"
+
+#: time.php:38
+msgid "No timeslices were selected to submit."
+msgstr "No se seleccionaron períodos a enviar."
+
+#: lib/api.php:291 lib/api.php:322
+msgid "Non-billable Hours"
+msgstr "Horas no facturables"
+
+#: lib/Hermes.php:217
+msgid "Not found."
+msgstr "No se encontró."
+
+#: lib/Driver.php:49 lib/Driver.php:66 lib/Driver.php:115 lib/Driver.php:133
+#: lib/Driver.php:146
+msgid "Not implemented."
+msgstr "No se ha desarrollado."
+
+#: lib/api.php:116
+msgid "Notes"
+msgstr "Observaciones"
+
+#: lib/Forms/Export.php:39
+msgid "QuickBooks (.iif)"
+msgstr "QuickBooks (.iif)"
+
+#: invoicing.php:41
+msgid "Rate"
+msgstr "Tasa"
+
+#: lib/Admin.php:132
+msgid "Really delete this job type? This may cause data problems!!"
+msgstr ""
+"¿Eliminar realmente este tipo de trabajo? ¡Puede ocasionar problemas de "
+"datos!"
+
+#: lib/Forms/Time.php:179
+msgid "Save"
+msgstr "Guardar"
+
+#: lib/Forms/Search.php:74
+msgid "Search"
+msgstr "Buscar"
+
+#: lib/Forms/Search.php:32
+msgid "Search For Time"
+msgstr "Buscar horas"
+
+#: search.php:123 lib/api.php:140
+msgid "Search Results"
+msgstr "Resultados de la búsqueda"
+
+#: lib/Block/tree_menu.php:29
+msgid "Search Time"
+msgstr "Buscar"
+
+#: search.php:92
+msgid "Search for Time"
+msgstr "Buscar horas"
+
+#: lib/Admin.php:190 lib/Forms/Time.php:219 lib/Forms/Time.php:258
+msgid "See Attached Timesheet"
+msgstr "Examine el horario adjunto"
+
+#: lib/Forms/Deliverable.php:22
+msgid "Select Client"
+msgstr "Seleccionar cliente"
+
+#: invoicing.php:76
+msgid "Select hours to be invoiced"
+msgstr "Seleccionar las horas a facturar"
+
+#: lib/Forms/Export.php:43
+msgid "Select the export format"
+msgstr "Seleccione el formato de exportación"
+
+#: config/.bak/prefs.php.dist:11
+msgid "Set preferences on the stop watch timer."
+msgstr "Define opciones del cronómetro."
+
+#: lib/Admin.php:190
+#, php-format
+msgid ""
+"Should users enter descriptions of their timeslices for this client? If not, "
+"the description will automatically be \"%s\"."
+msgstr ""
+"¿Hay que introducir la descripción de los periodos del cliente? Si no es el "
+"caso, de forma automática la descripción será \"%s\"."
+
+#: lib/Block/tree_stopwatch.php:28
+msgid "Start Watch"
+msgstr "Iniciar"
+
+#: start.php:20 start.php:44 lib/Block/tree_stopwatch.php:3
+msgid "Stop Watch"
+msgstr "Cronómetro"
+
+#: start.php:21
+msgid "Stop watch description"
+msgstr "Descripción"
+
+#: templates/time/form.html:5
+msgid "Submit Selected Time"
+msgstr "Enviar tiempo seleccionado"
+
+#: lib/Forms/Search.php:65
+msgid "Submitted?"
+msgstr "¿Enviadas?"
+
+#: lib/Hermes.php:227
+msgid "Summary"
+msgstr "Resumen"
+
+#: lib/Forms/Export.php:40
+msgid "Tab-Separated Variable (.tsv, .txt)"
+msgstr "Variables separadas por tabuladores (.tsv, .txt)"
+
+#: lib/Table.php:137
+msgid "Table"
+msgstr "Tabla"
+
+#: admin.php:175
+msgid "The client settings have been modified."
+msgstr "Se han modificado las opciones del cliente."
+
+#: admin.php:51
+#, php-format
+msgid "The job type \"%s\" has been added."
+msgstr "Se ha añadido el tipo de trabajo \"%s\"."
+
+#: admin.php:197
+msgid "The job type has been deleted."
+msgstr "Se ha eliminado el tipo de trabajo."
+
+#: admin.php:148
+msgid "The job type has been modified."
+msgstr "Se ha modificado el tipo de trabajo."
+
+#: admin.php:202
+msgid "The job type was not deleted."
+msgstr "No se eliminó el tipo de trabajo."
+
+#: start.php:40
+#, php-format
+msgid ""
+"The stop watch \"%s\" has been started and will appear in the sidebar at the "
+"next refresh."
+msgstr ""
+"Se ha iniciado el cronómetro \"%s\" y aparecerá en la barra lateral en la "
+"próxima actualización de la pantalla."
+
+#: entry.php:30
+#, php-format
+msgid "The stop watch \"%s\" has been stopped."
+msgstr "Se ha detenido el cronómetro \"%s\"."
+
+#: time.php:28 search.php:32
+msgid "The time entry was successfully deleted."
+msgstr "Se ha eliminado correctamente la entrada de hora."
+
+#: lib/Admin.php:157
+msgid "There are no clients to edit"
+msgstr "No hay clientes a modificar"
+
+#: lib/Forms/Time.php:63 lib/Forms/Deliverable.php:42
+msgid "There are no clients which you have access to."
+msgstr "No hay clientes a los que tenga acceso."
+
+#: lib/Forms/Time.php:49
+msgid "There are no job types configured."
+msgstr "No se han configurado tipos de trabajos."
+
+#: lib/Admin.php:64
+msgid "There are no job types to edit"
+msgstr "No hay tipos de trabajos a modificar."
+
+#: invoicing.php:28
+msgid "There is no submitted billable hours."
+msgstr "No se ha enviado ningún tiempo facturable."
+
+#: admin.php:53
+#, php-format
+msgid "There was an error adding the job type: %s."
+msgstr "Se produjo un error al añadir el tipo de trabajo: %s."
+
+#: admin.php:199
+#, php-format
+msgid "There was an error deleting the job type: %s."
+msgstr "Se produjo un error al eliminar el tipo de trabajo: %s."
+
+#: time.php:26 search.php:30
+#, php-format
+msgid "There was an error deleting the time: %s"
+msgstr "Se produjo un error al eliminar la hora: %s"
+
+#: admin.php:173
+#, php-format
+msgid "There was an error editing the client settings: %s."
+msgstr "Se produjo un error al modificar las opciones del cliente: %s."
+
+#: admin.php:150
+#, php-format
+msgid "There was an error editing the job type: %s."
+msgstr "Se produjo un error al modificar el tipo de trabajo: %s."
+
+#: entry.php:52
+#, php-format
+msgid "There was an error storing your timesheet: %s"
+msgstr "Se produjo un error al almacenar el horario: %s"
+
+#: time.php:46
+#, php-format
+msgid "There was an error submitting your time: %s"
+msgstr "Se produjo un error al enviar la hora: %s"
+
+#: lib/Admin.php:180
+msgid "This is not a valid client."
+msgstr "Éste no es un cliente válido."
+
+#: lib/Admin.php:87
+msgid "This is not a valid job type."
+msgstr "Éste no es un tipo de trabajo válido."
+
+#: lib/api.php:52
+msgid "Time Review Screen"
+msgstr "Pantalla de visualización de hora"
+
+#: config/.bak/prefs.php.dist:10
+msgid "Timer Options"
+msgstr "Opciones de temporización"
+
+#: invoicing.php:43
+msgid "Total"
+msgstr "Total"
+
+#: lib/api.php:297
+msgid "Total Hours"
+msgstr "Horas totales"
+
+#: lib/api.php:328
+#, php-format
+msgid "Total Hours for %s"
+msgstr "Horas totales de %s"
+
+#: lib/Forms/Time.php:248
+msgid "Update Submitted Time"
+msgstr "Actualizar hora enviada"
+
+#: lib/Forms/Time.php:173
+msgid "Update Time"
+msgstr "Actualizar hora"
+
+#: lib/Forms/Time.php:249
+msgid "Update time"
+msgstr "Actualizar hora"
+
+#: entry.php:28
+#, php-format
+msgid "Using the \"%s\" stop watch"
+msgstr "Utilizando el cronómetro \"%s\""
+
+#: invoicing.php:74 lib/api.php:72 lib/Admin.php:125 lib/Forms/Time.php:212
+#: lib/Forms/Time.php:301 lib/Forms/Export.php:48 lib/Forms/Search.php:63
+msgid "Yes"
+msgstr "Sí"
+
+#: entry.php:46
+msgid "Your time was successfully entered."
+msgstr "Se introdujo correctamente su tiempo."
+
+#: time.php:48
+msgid "Your time was successfully submitted."
+msgstr "Se envió correctamente su tiempo."
+
+#: entry.php:42
+msgid "Your time was successfully updated."
+msgstr "Se actualizó correctamente su tiempo."
+
+#: lib/Hermes.php:82
+msgid "_Admin"
+msgstr "A_dministrar"
+
+#: lib/Hermes.php:68
+msgid "_Deliverables"
+msgstr "_Prestaciones"
+
+#: lib/Hermes.php:72
+msgid "_Invoicing"
+msgstr "_Facturación"
+
+#: lib/Hermes.php:64
+msgid "_New Time"
+msgstr "_Añadir"
+
+#: lib/Hermes.php:77
+msgid "_Print"
+msgstr "_Imprimir"
+
+#: lib/Hermes.php:65
+msgid "_Search"
+msgstr "_Buscar"
+
+#: lib/api.php:199
+msgid "no client"
+msgstr "sin cliente"
diff --git a/hermes/po/fi_FI.po b/hermes/po/fi_FI.po
new file mode 100644 (file)
index 0000000..8a9dc75
--- /dev/null
@@ -0,0 +1,541 @@
+# Finnish translation for Hermes.
+# Copyright
+# Leena Heino <liinu@uta.fi>, 2004.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Hermes 0.1-cvs\n"
+"Report-Msgid-Bugs-To: dev@lists.horde.org\n"
+"POT-Creation-Date: 2005-03-14 11:02+0200\n"
+"PO-Revision-Date: 2004-12-10 12:59+0200\n"
+"Last-Translator: Leena Heino <liinu@uta.fi>\n"
+"Language-Team: Finnish <i18n@lists.horde.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=iso-8859-1\n"
+"Content-Transfer-Encoding: 8-bit\n"
+
+#: lib/Admin.php:39 lib/Search.php:71
+msgid " (DISABLED)"
+msgstr " (POIS KÄYTÖSTÄ)"
+
+#: templates/deliverables/list.inc:4
+#, fuzzy, php-format
+msgid "%s Deliverables"
+msgstr "Tapahtui virhe listattaessa asiakkaita: %s"
+
+#: lib/api.php:129
+#, php-format
+msgid "'%s' is not a defined table."
+msgstr "'%s' ei ole määritelty taulukko."
+
+#: lib/TimeForm.php:92
+msgid "--- No Deliverable ---"
+msgstr ""
+
+#: lib/Data/iif.php:67
+msgid "; Notes: "
+msgstr "; Muistiinpanot: "
+
+#: lib/Driver/sql.php:135
+msgid "Access denied; user cannot modify this timeslice."
+msgstr "Käyttö kielletty; käyttäjä ei voi muokata tämä aikasiivua."
+
+#: lib/DeliverableForm.php:59
+msgid "Active?"
+msgstr ""
+
+#: admin.php:57 admin.php:219
+msgid "Add Job Type"
+msgstr "Lisää työn tyyppi"
+
+#: lib/TimeForm.php:182 lib/TimeForm.php:265 lib/api.php:93
+msgid "Additional Notes"
+msgstr "Lisämuistiinpanot"
+
+#: admin.php:33
+msgid "Administration"
+msgstr "Ylläpito"
+
+#: lib/TimeForm.php:51 lib/Search.php:51 lib/DeliverableForm.php:37
+#, php-format
+msgid "An error occurred listing clients: %s"
+msgstr "Tapahtui virhe listattaessa asiakkaita: %s"
+
+#: lib/TimeForm.php:34 lib/Search.php:64
+#, php-format
+msgid "An error occurred listing job types: %s"
+msgstr "Tapahtui virhe listattaessa työn tyyppejä: %s"
+
+#: lib/Hermes.php:172
+#, php-format
+msgid "An error occurred listing users: %s"
+msgstr "Tapahtui virhe listattaessa käyttäjiä: %s"
+
+#: lib/TimeForm.php:70
+#, fuzzy, php-format
+msgid "An error occurred retrieving deliverables: %s"
+msgstr "Tapahtui virhe listattaessa asiakkaita: %s"
+
+#: lib/api.php:85
+msgid "Bill?"
+msgstr "Lasku?"
+
+#: lib/api.php:196
+msgid "Billable Hours"
+msgstr "Laskutettavat tunnit"
+
+#: lib/TimeForm.php:171 lib/TimeForm.php:261
+msgid "Billable?"
+msgstr "Laskutettavissa?"
+
+#: lib/Driver/sql.php:511
+msgid "Cannot delete deliverable; it has children."
+msgstr ""
+
+#: lib/Driver/sql.php:523
+msgid "Cannot delete deliverable; there is time entered on it."
+msgstr ""
+
+#: lib/Admin.php:154 lib/TimeForm.php:153 lib/TimeForm.php:251 lib/api.php:69
+#: lib/DeliverableForm.php:26
+msgid "Client"
+msgstr "Asiakas"
+
+#: lib/Admin.php:130
+msgid "Client Name"
+msgstr "Asiakkaan nimi"
+
+#: lib/Search.php:23
+msgid "Clients"
+msgstr "Asiakkaat"
+
+#: lib/Search.php:141
+msgid "Comma-Separated Variable (.csv)"
+msgstr "Pilkuilla erotetut muuttujat (.csv)"
+
+#: search.php:124
+msgid "Could not retrieve timeslice: invalid id"
+msgstr "Ei voitu hakea aikaviipaletta: epäkelpo id"
+
+#: lib/TimeForm.php:149 lib/TimeForm.php:247 lib/api.php:60
+msgid "Date"
+msgstr "Päiväys"
+
+#: lib/api.php:168 lib/api.php:169
+msgid "Delete Entry"
+msgstr "Poista merkintä"
+
+#: admin.php:86 admin.php:218
+msgid "Delete Job Type"
+msgstr "Poista työn tyyppi"
+
+#: admin.php:91 admin.php:206
+msgid "Delete Job Type: Confirmation"
+msgstr "Poista tuon tyyppi: Varmistus"
+
+#: deliverables.php:111
+#, fuzzy
+msgid "Delete This Deliverable"
+msgstr "Tapahtui virhe listattaessa asiakkaita: %s"
+
+#: scripts/purge.php:24
+#, php-format
+msgid "Deleting data that was exported/billed more than %s days ago.\n"
+msgstr "Poista tiedot, jotka viety/laskutettu %s päivää vanhempia.\n"
+
+#: lib/TimeForm.php:162 lib/api.php:76
+msgid "Deliverable"
+msgstr ""
+
+#: lib/Driver.php:85
+#, fuzzy, php-format
+msgid "Deliverable %d not found."
+msgstr "Aikamerkinnän poistaminen onnistui."
+
+#: lib/DeliverableForm.php:52
+msgid "Deliverable Detail"
+msgstr ""
+
+#: deliverables.php:40
+#, fuzzy
+msgid "Deliverable saved successfully."
+msgstr "Aikamerkinnän poistaminen onnistui."
+
+#: deliverables.php:53
+#, fuzzy
+msgid "Deliverable successfully deleted."
+msgstr "Aikamerkinnän poistaminen onnistui."
+
+#: deliverables.php:59 lib/api.php:31
+msgid "Deliverables"
+msgstr ""
+
+#: lib/TimeForm.php:180 lib/TimeForm.php:264 lib/api.php:91
+#: lib/DeliverableForm.php:61
+msgid "Description"
+msgstr "Kuvaus"
+
+#: lib/DeliverableForm.php:58
+msgid "Display Name"
+msgstr ""
+
+#: lib/Search.php:33
+msgid "Do not include entries after"
+msgstr "Älä sisällytä merkintöjä jälkeen"
+
+#: lib/Search.php:30
+msgid "Do not include entries before"
+msgstr "Älä sisällytä merkintöjä ennen"
+
+#: admin.php:128 admin.php:220
+msgid "Edit Client Settings"
+msgstr "Muokkaa asiakkaan asetuksia"
+
+#: admin.php:120 admin.php:179
+msgid "Edit Client Settings, Step 2"
+msgstr "Muokkaa asiakkaan asetuksia, askel 2"
+
+#: lib/api.php:166 lib/api.php:167
+msgid "Edit Entry"
+msgstr "Muokkaa merkintää"
+
+#: admin.php:73 admin.php:218
+msgid "Edit Job Type"
+msgstr "Muokkaa työn tyyppiä"
+
+#: admin.php:78
+msgid "Edit Job Type, Step 2"
+msgstr "Muokkaa työn tyyppiä, askel 2"
+
+#: admin.php:101
+msgid "Edit job type"
+msgstr "Muokkaa työn tyyppiä"
+
+#: admin.php:154
+msgid "Edit job type, Step 2"
+msgstr "Muokkaa työn tyyppiä, askel 2"
+
+#: lib/TimeForm.php:245 lib/api.php:65
+msgid "Employee"
+msgstr "Työntekijä"
+
+#: lib/Search.php:18
+msgid "Employees"
+msgstr "Työntekijät"
+
+#: lib/Admin.php:20 lib/Admin.php:80
+msgid "Enabled?"
+msgstr "Käytössä?"
+
+#: entry.php:61 lib/TimeForm.php:133
+msgid "Enter Time"
+msgstr "Anna aika"
+
+#: lib/TimeForm.php:134
+msgid "Enter my time"
+msgstr "Anna oma aika"
+
+#: deliverables.php:50
+#, fuzzy, php-format
+msgid "Error deleting deliverable: %s"
+msgstr "Tapahtui virhe listattaessa asiakkaita: %s"
+
+#: deliverables.php:36
+#, fuzzy, php-format
+msgid "Error saving deliverable: %s"
+msgstr "Tapahtui virhe listattaessa asiakkaita: %s"
+
+#: lib/DeliverableForm.php:60
+#, fuzzy
+msgid "Estimated Hours"
+msgstr "Laskutettavat tunnit"
+
+#: lib/Search.php:139
+msgid "Export Search Results"
+msgstr "Vie haun tulokset"
+
+#: lib/Search.php:43
+msgid "Exported?"
+msgstr "Viety"
+
+#: lib/TimeForm.php:167 lib/TimeForm.php:257 lib/api.php:79
+msgid "Hours"
+msgstr "Tunnit"
+
+#: lib/Admin.php:162
+msgid ""
+"ID for this client when exporting data, if different from the name displayed "
+"above."
+msgstr ""
+"ID tälle asiakkalle tietoja vietäessä, jos se on eri kuin yllä mainittu nimi."
+
+#: lib/Admin.php:19 lib/Admin.php:75 lib/Admin.php:102 lib/TimeForm.php:158
+#: lib/TimeForm.php:256 lib/api.php:73
+msgid "Job Type"
+msgstr "Työn tyyppi"
+
+#: lib/Search.php:27
+msgid "Job Types"
+msgstr "Työn tyypit"
+
+#: lib/Admin.php:50
+msgid "JobType Name"
+msgstr "Työn tyypin nimitys"
+
+#: lib/Search.php:152
+msgid "Mark the time as exported?"
+msgstr "Merkitse aika viedyksi?"
+
+#: time.php:54
+msgid "My Time"
+msgstr "Oma aika"
+
+#: time.php:75
+msgid "My Unsubmitted Time"
+msgstr "Oma lähettämätön aika"
+
+#: lib/Hermes.php:67
+msgid "My _Time"
+msgstr "Oma _Aika"
+
+#: deliverables.php:109
+msgid "New Sub-deliverable"
+msgstr ""
+
+#: templates/deliverables/list.inc:12
+msgid "New Top-level Deliverable"
+msgstr ""
+
+#: lib/Admin.php:98 lib/TimeForm.php:170 lib/TimeForm.php:260
+#: lib/Search.php:39 lib/Search.php:151 lib/api.php:48
+msgid "No"
+msgstr "Ei"
+
+#: lib/Driver.php:29
+#, php-format
+msgid "No job type with ID '%s'."
+msgstr "Ei työtyyppiä ID:llä '%s'."
+
+#: search.php:67 search.php:73
+msgid "No search to export!"
+msgstr "Ei hakuja vietäväksi!"
+
+#: search.php:83
+msgid "No time to export!"
+msgstr "Ei aikatietoja vietäväksi!"
+
+#: time.php:36
+msgid "No timeslices were selected to submit."
+msgstr "Ei aikaviipaleita merkitty lähetettäväksi."
+
+#: lib/api.php:201
+msgid "Non-billable Hours"
+msgstr "Ei laskutettavat tunnit"
+
+#: lib/Driver.php:50 lib/Driver.php:66 lib/Driver.php:115 lib/Driver.php:132
+#: lib/Driver.php:145
+msgid "Not implemented."
+msgstr "Ei toteutettu."
+
+#: lib/Search.php:143
+msgid "QuickBooks (.iif)"
+msgstr "QuickBooks (.iif)"
+
+#: lib/Admin.php:105
+msgid "Really delete this job type? This may cause data problems!!"
+msgstr "Poistaanko tämä työn tyyppi? Tämä voi aiheuttaa ongelmia!!"
+
+#: lib/Search.php:14
+msgid "Search For Time"
+msgstr "Etsi aikoja"
+
+#: search.php:152 lib/api.php:118
+msgid "Search Results"
+msgstr "Haun tulokset"
+
+#: search.php:115
+msgid "Search for Time"
+msgstr "Hae aikoja"
+
+#: lib/Admin.php:157 lib/TimeForm.php:177 lib/TimeForm.php:217
+msgid "See Attached Timesheet"
+msgstr "Katso liitettyä aikataulukkoa"
+
+#: lib/DeliverableForm.php:23
+#, fuzzy
+msgid "Select Client"
+msgstr "Asiakas"
+
+#: lib/Search.php:145
+msgid "Select the export format"
+msgstr "Valitse viennissä käytettävä muoto"
+
+#: lib/Admin.php:157
+#, php-format
+msgid ""
+"Should users enter descriptions of their timeslices for this client? If not, "
+"the description will automatically be \"%s\"."
+msgstr ""
+"Saavatko käyttäjät laittaa omia kuvauksia heidän aikaviipaleilleen tälle "
+"asiakkaalle? Jos ei niin, kuvaus on oletuksena \"%s\"."
+
+#: templates/time/form-footer.html:1
+msgid "Submit Selected Time"
+msgstr "Lähetä valitut ajat"
+
+#: lib/Search.php:40
+msgid "Submitted?"
+msgstr "Lähetetty?"
+
+#: lib/Search.php:142
+msgid "Tab-Separated Variable (.tsv,.txt)"
+msgstr "Tabulaattorilla erotetut muuttujat (.tsv,.txt)"
+
+#: admin.php:173
+msgid "The client settings have been modified."
+msgstr "Asiakkaan asetuksia on muokattu."
+
+#: admin.php:49
+#, php-format
+msgid "The job type '%s' has been added."
+msgstr "Työn tyyppi '%s' on lisätty."
+
+#: admin.php:195
+msgid "The job type has been deleted."
+msgstr "Työn tyyppi on poistettu."
+
+#: admin.php:146
+msgid "The job type has been modified."
+msgstr "Työn tyyppi on muokattu."
+
+#: admin.php:200
+msgid "The job type was not deleted."
+msgstr "Työn tyyppiä ei poistettu."
+
+#: search.php:29 time.php:26
+msgid "The time entry was successfully deleted."
+msgstr "Aikamerkinnän poistaminen onnistui."
+
+#: search.php:48
+msgid "The timeslice was successfully updated."
+msgstr "Aikaviipaleen päivitysä onnistui."
+
+#: lib/Admin.php:127
+msgid "There are no clients to edit"
+msgstr "Ei asiakkaita muokattavaksi"
+
+#: lib/TimeForm.php:57 lib/DeliverableForm.php:42
+msgid "There are no clients which you have access to."
+msgstr "Ei asiakkaita, joihin on pääsy sallittu."
+
+#: lib/TimeForm.php:43
+msgid "There are no job types configured."
+msgstr "Ei työn tyyppejä määriteltynä."
+
+#: lib/Admin.php:47
+msgid "There are no job types to edit"
+msgstr "Ei työn tyyppejä muokattavaksi"
+
+#: admin.php:51
+#, php-format
+msgid "There was an error adding the job type: %s."
+msgstr "Tapahtui virhe lisättäessä työn tyypiä: %s."
+
+#: admin.php:197
+#, php-format
+msgid "There was an error deleting the job type: %s."
+msgstr "Tapahtui virhe poistettaessa työn tyyppiä: %s."
+
+#: search.php:27 time.php:24
+#, php-format
+msgid "There was an error deleting the time: %s"
+msgstr "Tapahtui virhe poistettaessa aikaa: %s"
+
+#: admin.php:171
+#, php-format
+msgid "There was an error editing the client settings: %s."
+msgstr "Tapahtui virhe muokattaessa asiakkaan asetuksia: %s."
+
+#: admin.php:148
+#, php-format
+msgid "There was an error editing the job type: %s."
+msgstr "Tapahtui virhe muokattaessa työn tyyppiä: %s."
+
+#: search.php:46 entry.php:33
+#, php-format
+msgid "There was an error storing your timesheet: %s"
+msgstr "Tapahtui virhe talletettaessa aikataulukkoasi: %s"
+
+#: time.php:44
+#, php-format
+msgid "There was an error submitting your time: %s"
+msgstr "Tapahtui virhe talletettaessa aikaasi: %s"
+
+#: lib/Admin.php:147
+msgid "This is not a valid client."
+msgstr "Tämä ei ole kelvollinen asiakas."
+
+#: lib/Admin.php:67
+msgid "This is not a valid job type."
+msgstr "Tämä ei ole kelvollinen työn tyyppi"
+
+#: lib/api.php:29
+msgid "Time Review Screen"
+msgstr "Aikojen tarkistus ruutu"
+
+#: lib/api.php:205
+msgid "Total Hours"
+msgstr "Tunnit yhteensä"
+
+#: lib/TimeForm.php:207
+msgid "Update Submitted Time"
+msgstr "Päivitä lähetetty aika"
+
+#: lib/TimeForm.php:130
+msgid "Update Time"
+msgstr "Päivitä aika"
+
+#: lib/TimeForm.php:131
+msgid "Update my time"
+msgstr "Päivitä oma aika"
+
+#: lib/TimeForm.php:208
+msgid "Update time"
+msgstr "Päivitä aika"
+
+#: lib/Admin.php:98 lib/TimeForm.php:170 lib/TimeForm.php:260
+#: lib/Search.php:38 lib/Search.php:150 lib/api.php:47
+msgid "Yes"
+msgstr "Kyllä"
+
+#: entry.php:28
+msgid "Your time was successfully entered."
+msgstr "Aikasi merkittiin onnistuneesti."
+
+#: time.php:46
+msgid "Your time was successfully submitted."
+msgstr "Aikasi lähetettiin onnistuneesti."
+
+#: entry.php:25
+msgid "Your time was successfully updated."
+msgstr "Aikasi päivitettiin onnistuneesti."
+
+#: lib/Hermes.php:86
+msgid "_Admin"
+msgstr "_Ylläpito"
+
+#: lib/Hermes.php:76
+msgid "_Deliverables"
+msgstr ""
+
+#: lib/Hermes.php:68
+msgid "_Enter Time"
+msgstr "_Aseta aika"
+
+#: lib/Hermes.php:81
+msgid "_Print"
+msgstr "_Tulosta"
+
+#: lib/Hermes.php:69
+msgid "_Search"
+msgstr "_Haku"
diff --git a/hermes/po/hermes.pot b/hermes/po/hermes.pot
new file mode 100644 (file)
index 0000000..43199d1
--- /dev/null
@@ -0,0 +1,740 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright YEAR Horde Project
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: dev@lists.horde.org\n"
+"POT-Creation-Date: 2008-08-01 10:45+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: lib/Forms/Time.php:139
+#, php-format
+msgid " (%0.2f hours)"
+msgstr ""
+
+#: lib/Forms/Time.php:141
+#, php-format
+msgid " (%d%%, %0.2f of %0.2f hours)"
+msgstr ""
+
+#: lib/Admin.php:54 lib/Forms/Search.php:102
+msgid " (DISABLED)"
+msgstr ""
+
+#: lib/api.php:149
+#, php-format
+msgid "\"%s\" is not a defined table."
+msgstr ""
+
+#: templates/deliverables/list.inc:4
+#, php-format
+msgid "%s Deliverables"
+msgstr ""
+
+#: lib/Forms/Search.php:84
+msgid "- - None - -"
+msgstr ""
+
+#: lib/Forms/Time.php:111
+msgid "--- No Cost Object ---"
+msgstr ""
+
+#: lib/Forms/Time.php:61
+msgid "--- Select A Client ---"
+msgstr ""
+
+#: lib/Data/iif.php:64
+msgid "; Notes: "
+msgstr ""
+
+#: entry.php:74 lib/Driver/sql.php:142
+msgid "Access denied; user cannot modify this timeslice."
+msgstr ""
+
+#: lib/Forms/Deliverable.php:59
+msgid "Active?"
+msgstr ""
+
+#: admin.php:59 admin.php:221
+msgid "Add Job Type"
+msgstr ""
+
+#: config/prefs.php.dist:21
+msgid ""
+"Add stop watch name and start and end time to the description of the time "
+"entry?"
+msgstr ""
+
+#: lib/Forms/Time.php:237 lib/Forms/Time.php:325
+msgid "Additional Notes"
+msgstr ""
+
+#: admin.php:35
+msgid "Administration"
+msgstr ""
+
+#: lib/Forms/Time.php:57 lib/Forms/Search.php:81 lib/Forms/Deliverable.php:37
+#, php-format
+msgid "An error occurred listing clients: %s"
+msgstr ""
+
+#: lib/Forms/Time.php:40 lib/Forms/Search.php:95
+#, php-format
+msgid "An error occurred listing job types: %s"
+msgstr ""
+
+#: lib/Hermes.php:176
+#, php-format
+msgid "An error occurred listing users: %s"
+msgstr ""
+
+#: lib/api.php:335
+#, php-format
+msgid "An error occurred retrieving deliverables: %s"
+msgstr ""
+
+#: lib/api.php:289
+msgid "Approved By:"
+msgstr ""
+
+#: lib/api.php:106
+msgid "Bill?"
+msgstr ""
+
+#: lib/api.php:276 lib/api.php:302
+msgid "Billable Hours"
+msgstr ""
+
+#: lib/Admin.php:30 lib/Admin.php:102 lib/Forms/Time.php:226
+#: lib/Forms/Time.php:321 lib/Forms/Search.php:71
+msgid "Billable?"
+msgstr ""
+
+#: lib/Hermes.php:230
+msgid "By Client"
+msgstr ""
+
+#: lib/Hermes.php:232
+msgid "By Cost Object"
+msgstr ""
+
+#: lib/Hermes.php:228
+msgid "By Date"
+msgstr ""
+
+#: lib/Hermes.php:229
+msgid "By Employee"
+msgstr ""
+
+#: lib/Hermes.php:231
+msgid "By Job Type"
+msgstr ""
+
+#: lib/Driver/sql.php:581
+msgid "Cannot delete deliverable; it has children."
+msgstr ""
+
+#: lib/Driver/sql.php:596
+msgid "Cannot delete deliverable; there is time entered on it."
+msgstr ""
+
+#: invoicing.php:38 lib/Admin.php:187 lib/api.php:92 lib/Forms/Time.php:210
+#: lib/Forms/Time.php:311 lib/Forms/Deliverable.php:28
+msgid "Client"
+msgstr ""
+
+#: lib/Admin.php:160
+msgid "Client Name"
+msgstr ""
+
+#: lib/Forms/Search.php:42
+msgid "Clients"
+msgstr ""
+
+#: invoicing.php:75
+msgid "Combine same clients in one invoice"
+msgstr ""
+
+#: lib/Forms/Export.php:37
+msgid "Comma-Separated Variable (.csv)"
+msgstr ""
+
+#: invoicing.php:46 lib/api.php:98 lib/Forms/Time.php:219
+msgid "Cost Object"
+msgstr ""
+
+#: lib/Forms/Search.php:51
+msgid "Cost Objects"
+msgstr ""
+
+#: invoicing.php:70
+msgid "Create invoice"
+msgstr ""
+
+#: invoicing.php:44 lib/api.php:83 lib/Forms/Time.php:206
+#: lib/Forms/Time.php:307
+msgid "Date"
+msgstr ""
+
+#: lib/Forms/Time.php:183
+msgid "Delete"
+msgstr ""
+
+#: lib/Forms/Time.php:183
+msgid "Delete Entry"
+msgstr ""
+
+#: admin.php:88 admin.php:220
+msgid "Delete Job Type"
+msgstr ""
+
+#: admin.php:93 admin.php:208
+msgid "Delete Job Type: Confirmation"
+msgstr ""
+
+#: deliverables.php:107
+msgid "Delete This Deliverable"
+msgstr ""
+
+#: scripts/purge.php:26
+#, php-format
+msgid "Deleting data that was exported/billed more than %s days ago.\n"
+msgstr ""
+
+#: lib/Driver.php:85
+#, php-format
+msgid "Deliverable %d not found."
+msgstr ""
+
+#: lib/Forms/Deliverable.php:52
+msgid "Deliverable Detail"
+msgstr ""
+
+#: deliverables.php:42
+msgid "Deliverable saved successfully."
+msgstr ""
+
+#: deliverables.php:55
+msgid "Deliverable successfully deleted."
+msgstr ""
+
+#: deliverables.php:61 lib/api.php:54 lib/api.php:387
+msgid "Deliverables"
+msgstr ""
+
+#: invoicing.php:45 lib/api.php:112 lib/Forms/Time.php:235
+#: lib/Forms/Time.php:324 lib/Forms/Deliverable.php:61
+msgid "Description"
+msgstr ""
+
+#: lib/Forms/Deliverable.php:58
+msgid "Display Name"
+msgstr ""
+
+#: lib/Forms/Search.php:58
+msgid "Do not include entries after"
+msgstr ""
+
+#: lib/Forms/Search.php:55
+msgid "Do not include entries before"
+msgstr ""
+
+#: deliverables.php:103
+#, php-format
+msgid "Edit %s"
+msgstr ""
+
+#: admin.php:130 admin.php:222
+msgid "Edit Client Settings"
+msgstr ""
+
+#: admin.php:122 admin.php:181
+msgid "Edit Client Settings, Step 2"
+msgstr ""
+
+#: lib/Forms/Deliverable.php:30
+msgid "Edit Deliverables"
+msgstr ""
+
+#: lib/api.php:236 lib/api.php:237
+msgid "Edit Entry"
+msgstr ""
+
+#: admin.php:75 admin.php:220
+msgid "Edit Job Type"
+msgstr ""
+
+#: admin.php:80
+msgid "Edit Job Type, Step 2"
+msgstr ""
+
+#: entry.php:94
+msgid "Edit Time"
+msgstr ""
+
+#: admin.php:103
+msgid "Edit job type"
+msgstr ""
+
+#: admin.php:156
+msgid "Edit job type, Step 2"
+msgstr ""
+
+#: invoicing.php:39 lib/api.php:88 lib/Forms/Time.php:305
+msgid "Employee"
+msgstr ""
+
+#: lib/Forms/Search.php:36
+msgid "Employees"
+msgstr ""
+
+#: lib/Admin.php:28 lib/Admin.php:100
+msgid "Enabled?"
+msgstr ""
+
+#: lib/Block/tree_menu.php:3 lib/Block/tree_menu.php:21
+msgid "Enter Time"
+msgstr ""
+
+#: deliverables.php:52
+#, php-format
+msgid "Error deleting deliverable: %s"
+msgstr ""
+
+#: lib/Forms/Time.php:93 lib/Forms/Search.php:130
+#, php-format
+msgid "Error retrieving cost objects from \"%s\": %s"
+msgstr ""
+
+#: deliverables.php:38
+#, php-format
+msgid "Error saving deliverable: %s"
+msgstr ""
+
+#: lib/Driver/sql.php:298
+#, php-format
+msgid "Error: %s"
+msgstr ""
+
+#: lib/Forms/Deliverable.php:60
+msgid "Estimated Hours"
+msgstr ""
+
+#: lib/Forms/Export.php:56
+msgid "Export"
+msgstr ""
+
+#: lib/Forms/Export.php:35
+msgid "Export Search Results"
+msgstr ""
+
+#: lib/Forms/Search.php:68
+msgid "Exported?"
+msgstr ""
+
+#: config/prefs.php.dist:9
+msgid "General Options"
+msgstr ""
+
+#: lib/Admin.php:32 lib/Admin.php:104
+msgid "Hourly Rate"
+msgstr ""
+
+#: invoicing.php:42 lib/api.php:100 lib/Forms/Time.php:222
+#: lib/Forms/Time.php:317
+msgid "Hours"
+msgstr ""
+
+#: lib/Admin.php:195
+msgid ""
+"ID for this client when exporting data, if different from the name displayed "
+"above."
+msgstr ""
+
+#: lib/api.php:452
+msgid "Invalid entry: check data and retry."
+msgstr ""
+
+#: invoicing.php:122
+#, php-format
+msgid "Invoice for client %s successfuly created."
+msgstr ""
+
+#: lib/api.php:56
+msgid "Invoicing"
+msgstr ""
+
+#: invoicing.php:32
+msgid "Invoicing system is not installed."
+msgstr ""
+
+#: invoicing.php:40 lib/Admin.php:27 lib/Admin.php:95 lib/Admin.php:129
+#: lib/api.php:96 lib/Forms/Time.php:216 lib/Forms/Time.php:316
+msgid "Job Type"
+msgstr ""
+
+#: lib/Forms/Search.php:48
+msgid "Job Types"
+msgstr ""
+
+#: lib/Admin.php:67
+msgid "JobType Name"
+msgstr ""
+
+#: lib/Forms/Export.php:50
+msgid "Mark the time as exported?"
+msgstr ""
+
+#: lib/Forms/Export.php:38
+msgid "Microsoft Excel (.xls)"
+msgstr ""
+
+#: time.php:72
+msgid "My Time"
+msgstr ""
+
+#: time.php:62
+msgid "My Unsubmitted Time"
+msgstr ""
+
+#: lib/Hermes.php:63
+msgid "My _Time"
+msgstr ""
+
+#: deliverables.php:105
+msgid "New Sub-deliverable"
+msgstr ""
+
+#: entry.php:94 lib/Forms/Time.php:186
+msgid "New Time"
+msgstr ""
+
+#: templates/deliverables/list.inc:10
+msgid "New Top-level Deliverable"
+msgstr ""
+
+#: invoicing.php:74 lib/Admin.php:125 lib/api.php:71 lib/Forms/Time.php:225
+#: lib/Forms/Time.php:320 lib/Forms/Search.php:64 lib/Forms/Export.php:49
+msgid "No"
+msgstr ""
+
+#: lib/Driver.php:28
+#, php-format
+msgid "No job type with ID \"%s\"."
+msgstr ""
+
+#: search.php:54 search.php:60
+msgid "No search to export!"
+msgstr ""
+
+#: search.php:70
+msgid "No time to export!"
+msgstr ""
+
+#: time.php:38
+msgid "No timeslices were selected to submit."
+msgstr ""
+
+#: lib/api.php:282 lib/api.php:313
+msgid "Non-billable Hours"
+msgstr ""
+
+#: lib/Hermes.php:217
+msgid "Not found."
+msgstr ""
+
+#: lib/Driver.php:49 lib/Driver.php:66 lib/Driver.php:115 lib/Driver.php:133
+#: lib/Driver.php:146
+msgid "Not implemented."
+msgstr ""
+
+#: lib/api.php:114
+msgid "Notes"
+msgstr ""
+
+#: lib/Forms/Export.php:39
+msgid "QuickBooks (.iif)"
+msgstr ""
+
+#: invoicing.php:41
+msgid "Rate"
+msgstr ""
+
+#: lib/Admin.php:132
+msgid "Really delete this job type? This may cause data problems!!"
+msgstr ""
+
+#: lib/Forms/Time.php:188
+msgid "Save"
+msgstr ""
+
+#: lib/Forms/Search.php:74
+msgid "Search"
+msgstr ""
+
+#: lib/Forms/Search.php:32
+msgid "Search For Time"
+msgstr ""
+
+#: search.php:123 lib/api.php:138
+msgid "Search Results"
+msgstr ""
+
+#: lib/Block/tree_menu.php:29
+msgid "Search Time"
+msgstr ""
+
+#: search.php:92
+msgid "Search for Time"
+msgstr ""
+
+#: lib/Admin.php:190 lib/Forms/Time.php:232 lib/Forms/Time.php:277
+msgid "See Attached Timesheet"
+msgstr ""
+
+#: lib/Forms/Deliverable.php:22
+msgid "Select Client"
+msgstr ""
+
+#: invoicing.php:76
+msgid "Select hours to be invoiced"
+msgstr ""
+
+#: lib/Forms/Export.php:43
+msgid "Select the export format"
+msgstr ""
+
+#: config/prefs.php.dist:11
+msgid "Set preferences on the stop watch timer."
+msgstr ""
+
+#: lib/Admin.php:190
+#, php-format
+msgid ""
+"Should users enter descriptions of their timeslices for this client? If not, "
+"the description will automatically be \"%s\"."
+msgstr ""
+
+#: lib/Block/tree_stopwatch.php:28
+msgid "Start Watch"
+msgstr ""
+
+#: start.php:20 start.php:44 lib/Block/tree_stopwatch.php:3
+msgid "Stop Watch"
+msgstr ""
+
+#: start.php:21
+msgid "Stop watch description"
+msgstr ""
+
+#: templates/time/form.html:5
+msgid "Submit Selected Time"
+msgstr ""
+
+#: lib/Forms/Search.php:65
+msgid "Submitted?"
+msgstr ""
+
+#: lib/Hermes.php:227
+msgid "Summary"
+msgstr ""
+
+#: lib/Forms/Export.php:40
+msgid "Tab-Separated Variable (.tsv, .txt)"
+msgstr ""
+
+#: lib/Table.php:138
+msgid "Table"
+msgstr ""
+
+#: admin.php:175
+msgid "The client settings have been modified."
+msgstr ""
+
+#: admin.php:51
+#, php-format
+msgid "The job type \"%s\" has been added."
+msgstr ""
+
+#: admin.php:197
+msgid "The job type has been deleted."
+msgstr ""
+
+#: admin.php:148
+msgid "The job type has been modified."
+msgstr ""
+
+#: admin.php:202
+msgid "The job type was not deleted."
+msgstr ""
+
+#: start.php:40
+#, php-format
+msgid ""
+"The stop watch \"%s\" has been started and will appear in the sidebar at the "
+"next refresh."
+msgstr ""
+
+#: entry.php:31
+#, php-format
+msgid "The stop watch \"%s\" has been stopped."
+msgstr ""
+
+#: search.php:32 time.php:28
+msgid "The time entry was successfully deleted."
+msgstr ""
+
+#: lib/Admin.php:157
+msgid "There are no clients to edit"
+msgstr ""
+
+#: lib/Forms/Time.php:65 lib/Forms/Deliverable.php:42
+msgid "There are no clients which you have access to."
+msgstr ""
+
+#: lib/Forms/Time.php:49
+msgid "There are no job types configured."
+msgstr ""
+
+#: lib/Admin.php:64
+msgid "There are no job types to edit"
+msgstr ""
+
+#: invoicing.php:28
+msgid "There is no submitted billable hours."
+msgstr ""
+
+#: admin.php:53
+#, php-format
+msgid "There was an error adding the job type: %s."
+msgstr ""
+
+#: admin.php:199
+#, php-format
+msgid "There was an error deleting the job type: %s."
+msgstr ""
+
+#: search.php:30 time.php:26
+#, php-format
+msgid "There was an error deleting the time: %s"
+msgstr ""
+
+#: admin.php:173
+#, php-format
+msgid "There was an error editing the client settings: %s."
+msgstr ""
+
+#: admin.php:150
+#, php-format
+msgid "There was an error editing the job type: %s."
+msgstr ""
+
+#: entry.php:53
+#, php-format
+msgid "There was an error storing your timesheet: %s"
+msgstr ""
+
+#: time.php:46
+#, php-format
+msgid "There was an error submitting your time: %s"
+msgstr ""
+
+#: lib/Admin.php:180
+msgid "This is not a valid client."
+msgstr ""
+
+#: lib/Admin.php:87
+msgid "This is not a valid job type."
+msgstr ""
+
+#: lib/api.php:52
+msgid "Time Review Screen"
+msgstr ""
+
+#: config/prefs.php.dist:10
+msgid "Timer Options"
+msgstr ""
+
+#: invoicing.php:43
+msgid "Total"
+msgstr ""
+
+#: lib/api.php:288
+msgid "Total Hours"
+msgstr ""
+
+#: lib/api.php:319
+#, php-format
+msgid "Total Hours for %s"
+msgstr ""
+
+#: lib/Forms/Time.php:267
+msgid "Update Submitted Time"
+msgstr ""
+
+#: lib/Forms/Time.php:182
+msgid "Update Time"
+msgstr ""
+
+#: lib/Forms/Time.php:268
+msgid "Update time"
+msgstr ""
+
+#: entry.php:29
+#, php-format
+msgid "Using the \"%s\" stop watch from %s to %s"
+msgstr ""
+
+#: invoicing.php:74 lib/Admin.php:125 lib/api.php:70 lib/Forms/Time.php:225
+#: lib/Forms/Time.php:320 lib/Forms/Search.php:63 lib/Forms/Export.php:48
+msgid "Yes"
+msgstr ""
+
+#: entry.php:47
+msgid "Your time was successfully entered."
+msgstr ""
+
+#: time.php:48
+msgid "Your time was successfully submitted."
+msgstr ""
+
+#: entry.php:43
+msgid "Your time was successfully updated."
+msgstr ""
+
+#: lib/Hermes.php:82
+msgid "_Admin"
+msgstr ""
+
+#: lib/Hermes.php:68
+msgid "_Deliverables"
+msgstr ""
+
+#: lib/Hermes.php:72
+msgid "_Invoicing"
+msgstr ""
+
+#: lib/Hermes.php:64
+msgid "_New Time"
+msgstr ""
+
+#: lib/Hermes.php:77
+msgid "_Print"
+msgstr ""
+
+#: lib/Hermes.php:65
+msgid "_Search"
+msgstr ""
+
+#: lib/api.php:194
+msgid "no client"
+msgstr ""
diff --git a/hermes/po/zh_TW.po b/hermes/po/zh_TW.po
new file mode 100644 (file)
index 0000000..81ae1c2
--- /dev/null
@@ -0,0 +1,416 @@
+# Hermes Traditional Chinese Translation
+# Copyright 2002 David Chang.±i¨}¤å,¥xÆW
+# David Chang <david@tmv.gov.tw>, 2002.
+msgid ""
+msgstr ""
+"Project-Id-Version: Hermes 0.1-cvs\n"
+"Report-Msgid-Bugs-To: dev@lists.horde.org\n"
+"POT-Creation-Date: 2004-11-22 16:41+0800\n"
+"PO-Revision-Date: 2003-01-08 10:53+0100\n"
+"Last-Translator: David Chang <david@thbuo.gov.tw>\n"
+"Language-Team: Traditional Chinese <david@thbuo.gov.tw>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=BIG5\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: lib/api.php:116
+#, php-format
+msgid "'%s' is not a defined table."
+msgstr "'%s' ¤£¬O¤@­Ó¤w©w¸qªºªí®æ."
+
+#: lib/Data/iif.php:67
+msgid "; Notes: "
+msgstr "; ³Æµù: "
+
+#: lib/Driver/sql.php:130
+msgid "Access denied; user cannot modify this timeslice."
+msgstr "¦s¨ú¾D©Ú;¨Ï¥ÎªÌµLªk­×§ï³o­Ó®É¬q."
+
+#: admin.php:56 admin.php:217
+msgid "Add Job Type"
+msgstr "·s¼W¾·~«¬ºA"
+
+#: lib/TimeForm.php:128 lib/TimeForm.php:211 lib/api.php:87
+msgid "Additional Notes"
+msgstr "¥[µù"
+
+#: admin.php:33
+msgid "Administration"
+msgstr "ºÞ²z"
+
+#: lib/Search.php:49 lib/TimeForm.php:47
+#, php-format
+msgid "An error occurred listing clients: %s"
+msgstr "¦C¥X«È¤á: %s ®Éµo¥Í¤@­Ó¿ù»~"
+
+#: lib/Search.php:62 lib/TimeForm.php:34
+#, php-format
+msgid "An error occurred listing job types: %s"
+msgstr "¦C¥X¾·~«¬ºA: %s ®Éµo¥Í¤@­Ó¿ù»~"
+
+#: lib/Hermes.php:162
+#, php-format
+msgid "An error occurred listing users: %s"
+msgstr "¦C¥X¨Ï¥ÎªÌ: %s ®Éµo¥Í¤@­Ó¿ù»~"
+
+#: lib/api.php:79
+msgid "Bill?"
+msgstr "¤ä¥I?"
+
+#: lib/api.php:184
+msgid "Billable Hours"
+msgstr "¥i¤ä¥I®É¼Æ?"
+
+#: lib/TimeForm.php:124 lib/TimeForm.php:207
+msgid "Billable?"
+msgstr "¤ä¥I?"
+
+#: lib/Admin.php:143 lib/TimeForm.php:114 lib/TimeForm.php:197 lib/api.php:67
+msgid "Client"
+msgstr "«È¤á"
+
+#: lib/Admin.php:119
+msgid "Client Name"
+msgstr "«È¤á¦WºÙ"
+
+#: lib/Search.php:23
+msgid "Clients"
+msgstr "«È¤á"
+
+#: lib/Search.php:132
+msgid "Comma-Separated Variable (.csv)"
+msgstr "³rÂI¤À¹j­È (.csv)"
+
+#: search.php:76
+msgid "Could not retrieve timeslice: invalid id"
+msgstr "µLªkŪ¨ú®É¬q: µL®ÄªºÃѧO¸¹½X(id)"
+
+#: lib/TimeForm.php:111 lib/TimeForm.php:193 lib/api.php:58
+msgid "Date"
+msgstr "¤é´Á"
+
+#: lib/api.php:155 lib/api.php:156
+msgid "Delete Entry"
+msgstr "§R°£¬ö¿ý"
+
+#: admin.php:85 admin.php:216
+msgid "Delete Job Type"
+msgstr "§R°£Â¾·~«¬ºA"
+
+#: admin.php:90 admin.php:204
+msgid "Delete Job Type: Confirmation"
+msgstr "§R°£Â¾·~«¬ºA: ½T»{"
+
+#: scripts/purge.php:24
+#, php-format
+msgid "Deleting data that was exported/billed more than %s days ago.\n"
+msgstr "§R°£ %s ¤Ñ«e©Ò¦³¤w¶×¥X/¤w¤ä¥Iªº¸ê®Æ.\n"
+
+#: lib/TimeForm.php:127 lib/TimeForm.php:210 lib/api.php:85
+msgid "Description"
+msgstr "´y­z"
+
+#: lib/Search.php:32
+msgid "Do not include entries after"
+msgstr "¤£­n¥]¬A¤§«áªº¬ö¿ý"
+
+#: lib/Search.php:30
+msgid "Do not include entries before"
+msgstr "¤£­n¥]¬A¤§«eªº¬ö¿ý"
+
+#: admin.php:127 admin.php:218
+msgid "Edit Client Settings"
+msgstr "½s¿è«È¤áªº³]©w"
+
+#: admin.php:119 admin.php:177
+msgid "Edit Client Settings, Step 2"
+msgstr "½s¿è«È¤áªº³]©w, ¨BÆJ 2"
+
+#: lib/api.php:153 lib/api.php:154
+msgid "Edit Entry"
+msgstr "½s¿è¬ö¿ý"
+
+#: admin.php:72 admin.php:216
+msgid "Edit Job Type"
+msgstr "½s¿è¾·~«¬ºA"
+
+#: admin.php:77
+msgid "Edit Job Type, Step 2"
+msgstr "½s¿è¾·~«¬ºA, ¨BÆJ 2"
+
+#: admin.php:100
+msgid "Edit job type"
+msgstr "½s¿è¾·~«¬ºA"
+
+#: admin.php:152
+msgid "Edit job type, Step 2"
+msgstr "½s¿è¾·~«¬ºA, ¨BÆJ 2"
+
+#: lib/TimeForm.php:191 lib/api.php:63
+msgid "Employee"
+msgstr "­û¤u"
+
+#: lib/Search.php:18
+msgid "Employees"
+msgstr "­û¤u"
+
+#: entry.php:61 lib/TimeForm.php:82
+msgid "Enter Time"
+msgstr "¿é¤J®É¶¡"
+
+#: lib/TimeForm.php:83
+msgid "Enter my time"
+msgstr "¿é¤J§Úªº®É¶¡"
+
+#: lib/Search.php:130
+msgid "Export Search Results"
+msgstr "¶×¥X·j´Mµ²ªG"
+
+#: lib/Search.php:41
+msgid "Exported?"
+msgstr "¤w¶×¥X ?"
+
+#: lib/TimeForm.php:120 lib/TimeForm.php:203 lib/api.php:73
+msgid "Hours"
+msgstr "®É"
+
+#: lib/Admin.php:151
+msgid ""
+"ID for this client when exporting data, if different from the name displayed "
+"above."
+msgstr ""
+
+#: lib/Admin.php:19 lib/Admin.php:66 lib/Admin.php:91 lib/TimeForm.php:119
+#: lib/TimeForm.php:202 lib/api.php:71
+msgid "Job Type"
+msgstr "¾·~«¬ºA"
+
+#: lib/Search.php:27
+msgid "Job Types"
+msgstr "¾·~«¬ºA"
+
+#: lib/Admin.php:41
+msgid "JobType Name"
+msgstr "¾·~«¬ºAªº¦WºÙ"
+
+#: lib/Search.php:143
+msgid "Mark the time as exported?"
+msgstr "±N¦¹®É¶¡¼Ð°O¦¨¤w¶×¥X ?"
+
+#: time.php:54
+msgid "My Time"
+msgstr "§Úªº®É¶¡"
+
+#: time.php:75
+msgid "My Unsubmitted Time"
+msgstr "§Ú©|¥¼°e¥Xªº®É¶¡"
+
+#: lib/Hermes.php:65
+msgid "My _Time"
+msgstr "§Úªº®É¶¡"
+
+#: lib/Admin.php:87 lib/Search.php:37 lib/Search.php:142 lib/TimeForm.php:123
+#: lib/TimeForm.php:206 lib/api.php:46
+msgid "No"
+msgstr "§_"
+
+#: time.php:36
+msgid "No timeslices were selected to submit."
+msgstr "°e¥X®É¨S¦³¿ï¾Ü®É¬q."
+
+#: lib/api.php:189
+msgid "Non-billable Hours"
+msgstr "¥¼¤ä¥I®É¼Æ."
+
+#: lib/Search.php:134
+msgid "QuickBooks (.iif)"
+msgstr ""
+
+#: lib/Admin.php:94
+msgid "Really delete this job type? This may cause data problems!!"
+msgstr "½T©w­n§R°£¦¹Â¾·~«¬ºA? ³o»ò§@¦³¥i¯à³y¦¨¸ê®Æ¤Wªº°ÝÃD!!"
+
+#: lib/Search.php:14
+msgid "Search For Time"
+msgstr "·j´M®É¶¡"
+
+#: search.php:104 lib/api.php:105
+msgid "Search Results"
+msgstr "·j´Mµ²ªG"
+
+#: search.php:67
+msgid "Search for Time"
+msgstr "·j´M®É¶¡"
+
+#: lib/Admin.php:146 lib/TimeForm.php:93 lib/TimeForm.php:163
+msgid "See Attached Timesheet"
+msgstr "°Ñ¾\ªþ¥[ªº®É¶¡¬ö¿ý¥d"
+
+#: lib/Search.php:136
+msgid "Select the export format"
+msgstr "¿ï¾Ü¶×¥X®æ¦¡"
+
+#: lib/Admin.php:146
+#, php-format
+msgid ""
+"Should users enter descriptions of their timeslices for this client? If not, "
+"the description will automatically be \"%s\"."
+msgstr ""
+"¨Ï¥ÎªÌ¬O§_À³¸Ó¬°¦¹«È¤á¿é¤J¥L­Ìªº®É¬q´y­z ? ¦pªG¤£¬O,´y­z±N¦Û°Ê¥H\"%s\"¥N´À."
+
+#: templates/time/form-footer.html:1
+msgid "Submit Selected Time"
+msgstr "°e¥X©Ò¿ï¾Üªº®É¶¡"
+
+#: lib/Search.php:38
+msgid "Submitted?"
+msgstr "¤w°e¥X ?"
+
+#: lib/Search.php:133
+msgid "Tab-Separated Variable (.tsv,.txt)"
+msgstr ""
+
+#: admin.php:171
+msgid "The client settings have been modified."
+msgstr "«È¤áªº³]©w¤w­×§ï."
+
+#: admin.php:48
+#, php-format
+msgid "The job type '%s' has been added."
+msgstr "¾·~«¬ºA '%s' ¤w·s¼W."
+
+#: admin.php:193
+msgid "The job type has been deleted."
+msgstr "¾·~«¬ºA '%s' ¤w§R°£."
+
+#: admin.php:144
+msgid "The job type has been modified."
+msgstr "¾·~«¬ºA¤w­×§ï."
+
+#: admin.php:198
+msgid "The job type was not deleted."
+msgstr "¾·~«¬ºA¥¼§R°£."
+
+#: search.php:29 time.php:26
+msgid "The time entry was successfully deleted."
+msgstr "®É¶¡¬ö¿ý¤w§R°£§¹¦¨."
+
+#: search.php:48
+msgid "The timeslice was successfully updated."
+msgstr "®É¬q¤wÅܧ󧹦¨."
+
+#: lib/Admin.php:116
+msgid "There are no clients to edit"
+msgstr "¨S¦³«È¤á¥i¨Ñ½s¿è"
+
+#: lib/TimeForm.php:52
+msgid "There are no clients which you have access to."
+msgstr "§A¥Ø«e©|¥¼¦s¨ú¥ô¦ó«È¤á."
+
+#: lib/TimeForm.php:39
+msgid "There are no job types configured."
+msgstr "¥¼³]©w¥ô¦óªºÂ¾·~«¬ºA."
+
+#: lib/Admin.php:38
+msgid "There are no job types to edit"
+msgstr "¥¼½s¿è¥ô¦óªºÂ¾·~«¬ºA"
+
+#: admin.php:50
+#, php-format
+msgid "There was an error adding the job type: %s."
+msgstr "·s¼W¾·~«¬ºA: %s ®Éµo¥Í¤@­Ó¿ù»~."
+
+#: admin.php:195
+#, php-format
+msgid "There was an error deleting the job type: %s."
+msgstr "§R°£Â¾·~«¬ºA: %s ®Éµo¥Í¤@­Ó¿ù»~."
+
+#: search.php:27 time.php:24
+#, php-format
+msgid "There was an error deleting the time: %s"
+msgstr "§R°£®É¶¡: %s ®Éµo¥Í¤@­Ó¿ù»~."
+
+#: admin.php:169
+#, php-format
+msgid "There was an error editing the client settings: %s."
+msgstr "½s¿è«È¤á³]©w: %s ®Éµo¥Í¤@­Ó¿ù»~."
+
+#: admin.php:146
+#, php-format
+msgid "There was an error editing the job type: %s."
+msgstr "½s¿è¾·~«¬ºA: %s ®Éµo¥Í¤@­Ó¿ù»~."
+
+#: entry.php:33 search.php:46
+#, php-format
+msgid "There was an error storing your timesheet: %s"
+msgstr "Àx¦s®É¶¡¬ö¿ý¥d: %s ®Éµo¥Í¤@­Ó¿ù»~"
+
+#: time.php:44
+#, php-format
+msgid "There was an error submitting your time: %s"
+msgstr "°e¥X®É¶¡: %s ®Éµo¥Í¤@­Ó¿ù»~"
+
+#: lib/Admin.php:136
+msgid "This is not a valid client."
+msgstr "³o¤£¬O¤@­Ó¦³®Äªº«È¤á."
+
+#: lib/Admin.php:58
+msgid "This is not a valid job type."
+msgstr "³o¤£¬O¤@­Ó¦³®ÄªºÂ¾·~«¬ºA."
+
+#: lib/api.php:29
+msgid "Time Review Screen"
+msgstr "®É¶¡¦^ÅUµe­±"
+
+#: lib/api.php:193
+msgid "Total Hours"
+msgstr "Á`®É¼Æ"
+
+#: lib/TimeForm.php:153
+msgid "Update Submitted Time"
+msgstr "Åܧó°e¥Xªº®É¶¡"
+
+#: lib/TimeForm.php:79
+msgid "Update Time"
+msgstr "Åܧó®É¶¡"
+
+#: lib/TimeForm.php:80
+msgid "Update my time"
+msgstr "Åܧó§Úªº®É¶¡"
+
+#: lib/TimeForm.php:154
+msgid "Update time"
+msgstr "Åܧó®É¶¡"
+
+#: lib/Admin.php:87 lib/Search.php:36 lib/Search.php:141 lib/TimeForm.php:123
+#: lib/TimeForm.php:206 lib/api.php:45
+msgid "Yes"
+msgstr "¬O"
+
+#: entry.php:28
+msgid "Your time was successfully entered."
+msgstr "§Aªº®É¶¡¤w¿é¤J§¹¦¨."
+
+#: time.php:46
+msgid "Your time was successfully submitted."
+msgstr "§Aªº®É¶¡¤w°e¥X§¹¦¨."
+
+#: entry.php:25
+msgid "Your time was successfully updated."
+msgstr "§Aªº®É¶¡¤wÅܧ󧹦¨."
+
+#: lib/Hermes.php:76
+msgid "_Admin"
+msgstr "ºÞ²z_A"
+
+#: lib/Hermes.php:66
+msgid "_Enter Time"
+msgstr "¿é¤J®É¶¡_E"
+
+#: lib/Hermes.php:71
+msgid "_Print"
+msgstr "¦C¦L_P"
+
+#: lib/Hermes.php:67
+msgid "_Search"
+msgstr "·j´M_S"
diff --git a/hermes/scripts/.htaccess b/hermes/scripts/.htaccess
new file mode 100644 (file)
index 0000000..3a42882
--- /dev/null
@@ -0,0 +1 @@
+Deny from all
diff --git a/hermes/scripts/Sandals.wdgt/Default.png b/hermes/scripts/Sandals.wdgt/Default.png
new file mode 100644 (file)
index 0000000..8180b3f
Binary files /dev/null and b/hermes/scripts/Sandals.wdgt/Default.png differ
diff --git a/hermes/scripts/Sandals.wdgt/Icon.png b/hermes/scripts/Sandals.wdgt/Icon.png
new file mode 100644 (file)
index 0000000..e7e8a85
Binary files /dev/null and b/hermes/scripts/Sandals.wdgt/Icon.png differ
diff --git a/hermes/scripts/Sandals.wdgt/Info.plist b/hermes/scripts/Sandals.wdgt/Info.plist
new file mode 100644 (file)
index 0000000..10636d4
--- /dev/null
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- $Horde: hermes/scripts/Sandals.wdgt/Info.plist,v 1.2 2008/03/08 06:52:37 bklang Exp $ -->
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+       <key>AllowMultipleInstances</key>
+       <true/>
+       <key>AllowNetworkAccess</key>
+       <true/>
+       <key>AllowSystem</key>
+       <true/>
+       <key>CFBundleDisplayName</key>
+       <string>Horde Sandals</string>
+       <key>CFBundleIdentifier</key>
+       <string>org.horde.widget.sandals</string>
+       <key>CFBundleName</key>
+       <string>Horde Sandals</string>
+       <key>CFBundleShortVersionString</key>
+       <string>0.0.1</string>
+       <key>CFBundleVersion</key>
+       <string>0.0.1</string>
+       <key>CloseBoxInsetX</key>
+       <integer>5</integer>
+       <key>CloseBoxInsetY</key>
+       <integer>5</integer>
+       <key>MainHTML</key>
+       <string>Sandals.html</string>
+       <key>Plugin</key>
+       <string>KeychainPlugIn.widgetplugin</string>
+</dict>
+</plist>
diff --git a/hermes/scripts/Sandals.wdgt/KeychainPlugIn.widgetplugin/Contents/Info.plist b/hermes/scripts/Sandals.wdgt/KeychainPlugIn.widgetplugin/Contents/Info.plist
new file mode 100644 (file)
index 0000000..b11b898
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- $Horde: hermes/scripts/Sandals.wdgt/KeychainPlugIn.widgetplugin/Contents/Info.plist,v 1.2 2008/03/08 06:52:37 bklang Exp $ -->
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+       <key>CFBundleDevelopmentRegion</key>
+       <string>English</string>
+       <key>CFBundleExecutable</key>
+       <string>KeychainPlugIn</string>
+       <key>CFBundleIdentifier</key>
+       <string>org.horde.dashboard.KeychainPlugin</string>
+       <key>CFBundleInfoDictionaryVersion</key>
+       <string>1.0</string>
+       <key>CFBundleName</key>
+       <string>KeychainPlugIn</string>
+       <key>CFBundlePackageType</key>
+       <string>BNDL</string>
+       <key>CFBundleSignature</key>
+       <string>????</string>
+       <key>CFBundleVersion</key>
+       <string>1.0</string>
+</dict>
+</plist>
diff --git a/hermes/scripts/Sandals.wdgt/KeychainPlugIn.widgetplugin/Contents/MacOS/KeychainPlugIn b/hermes/scripts/Sandals.wdgt/KeychainPlugIn.widgetplugin/Contents/MacOS/KeychainPlugIn
new file mode 100755 (executable)
index 0000000..66a52ab
Binary files /dev/null and b/hermes/scripts/Sandals.wdgt/KeychainPlugIn.widgetplugin/Contents/MacOS/KeychainPlugIn differ
diff --git a/hermes/scripts/Sandals.wdgt/KeychainPlugIn.widgetplugin/Contents/Resources/English.lproj/InfoPlist.strings b/hermes/scripts/Sandals.wdgt/KeychainPlugIn.widgetplugin/Contents/Resources/English.lproj/InfoPlist.strings
new file mode 100644 (file)
index 0000000..7934d23
Binary files /dev/null and b/hermes/scripts/Sandals.wdgt/KeychainPlugIn.widgetplugin/Contents/Resources/English.lproj/InfoPlist.strings differ
diff --git a/hermes/scripts/Sandals.wdgt/Sandals.html b/hermes/scripts/Sandals.wdgt/Sandals.html
new file mode 100644 (file)
index 0000000..f206413
--- /dev/null
@@ -0,0 +1,437 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<!-- $Horde: hermes/scripts/Sandals.wdgt/Sandals.html,v 1.5 2008/03/11 22:57:38 bklang Exp $ -->
+<!-- Sandals copyright 2007-2008, Alkaloid Networks LLC. -->
+<!-- Sandals is released under the terms of the GNU Public License v2 -->
+<!-- Sandals is a part of the Hermes module for the Horde framework. -->
+<!-- Horde Project: http://www.horde.org/ | Hermes: http://www.horde.org/hermes/ -->
+<!-- Horde Licenses: http://www.horde.org/licenses/ -->
+
+<html lang="en-US" xmlns="http://www.w3.org/1999/xhtml" xml:lang=
+"en-US">
+<head>
+  <title>Sandals: Horde Timetracking</title>
+<!-- Widget Libraries -->
+<script type='text/javascript' 
+  src='/System/Library/WidgetResources/AppleClasses/AppleInfoButton.js' 
+  charset='utf-8' /> 
+<script type='text/javascript' 
+  src='/System/Library/WidgetResources/AppleClasses/AppleAnimator.js' 
+  charset='utf-8' /> 
+<script type='text/javascript' 
+  src='/System/Library/WidgetResources/AppleClasses/AppleButton.js' 
+  charset='utf-8' /> 
+<!-- End Widget Libs -->
+<script type="text/javascript" src="lib/vcXMLRPC.js" charset=
+"utf-8">
+</script>
+<script type="text/javascript" src="lib/horde.js">
+</script>
+<script type="text/javascript" src="lib/Sandals.js" charset=
+"utf-8">
+</script>
+  <link href="themes/screen.css" rel="stylesheet" type=
+  "text/css" />
+  <link href="themes/bluewhite-screen.css" rel="stylesheet" type=
+  "text/css" />
+  <link href="themes/hermes-screen.css" rel="stylesheet" type=
+  "text/css" />
+  <link href="themes/sandals-screen.css" rel="stylesheet" type=
+  "text/css" />
+</head>
+
+<body onload="setup();" background="Default.png">
+  <div id="front">
+    <div id="notificationBox"></div>
+    <div id='NewTime'>
+      <!-- form action="#" name="timeentryform" id="timeentryform" -->
+        <div class="form" id="timeentryform_active">
+          <div class="header">
+            Horde Time Entry
+          </div>
+          <script type="text/javascript" src="lib/stripe.js"></script>
+          <ul id='msgBox' class='notices'></ul>
+
+          <div id="timeentryform_section___base" style=
+          "display:block;">
+            <table class="striped" cellspacing="0">
+              <tr valign="top">
+                <td width="15%" align="right"><span class=
+                "form-error"><img src=
+                "themes/graphics/required.png" alt="*" title=
+                "" /></span>&nbsp;Date</td>
+
+                <td>
+                  <script type="text/javascript" src=
+                  "lib/open_calendar.js">
+</script> <select name="date_month" id="date_month">
+                  <option value="">
+                      MM
+                    </option>
+
+                    <option value="1">
+                      January
+                    </option>
+
+                    <option value="2">
+                      February
+                    </option>
+
+                    <option value="3">
+                      March
+                    </option>
+
+                    <option value="4">
+                      April
+                    </option>
+
+                    <option value="5">
+                      May
+                    </option>
+
+                    <option value="6">
+                      June
+                    </option>
+                    <!-- FIXME: Use JS to pre-populate the current date -->
+                    <option value="7" selected="selected">
+                      July
+                    </option>
+
+                    <option value="8">
+                      August
+                    </option>
+
+                    <option value="9">
+                      September
+                    </option>
+
+                    <option value="10">
+                      October
+                    </option>
+
+                    <option value="11">
+                      November
+                    </option>
+
+                    <option value="12">
+                      December
+                    </option>
+                  </select><select name="date_day" id="date_day">
+                    <option value="">
+                      DD
+                    </option>
+
+                    <option value="1">
+                      1
+                    </option>
+
+                    <option value="2" selected="selected">
+                      2
+                    </option>
+
+                    <option value="3">
+                      3
+                    </option>
+
+                    <option value="4">
+                      4
+                    </option>
+
+                    <option value="5">
+                      5
+                    </option>
+
+                    <option value="6">
+                      6
+                    </option>
+
+                    <option value="7">
+                      7
+                    </option>
+
+                    <option value="8">
+                      8
+                    </option>
+
+                    <option value="9">
+                      9
+                    </option>
+
+                    <option value="10">
+                      10
+                    </option>
+
+                    <option value="11">
+                      11
+                    </option>
+
+                    <option value="12">
+                      12
+                    </option>
+
+                    <option value="13">
+                      13
+                    </option>
+
+                    <option value="14">
+                      14
+                    </option>
+
+                    <option value="15">
+                      15
+                    </option>
+
+                    <option value="16">
+                      16
+                    </option>
+
+                    <option value="17">
+                      17
+                    </option>
+
+                    <option value="18">
+                      18
+                    </option>
+
+                    <option value="19">
+                      19
+                    </option>
+
+                    <option value="20">
+                      20
+                    </option>
+
+                    <option value="21">
+                      21
+                    </option>
+
+                    <option value="22">
+                      22
+                    </option>
+
+                    <option value="23">
+                      23
+                    </option>
+
+                    <option value="24">
+                      24
+                    </option>
+
+                    <option value="25">
+                      25
+                    </option>
+
+                    <option value="26">
+                      26
+                    </option>
+
+                    <option value="27">
+                      27
+                    </option>
+
+                    <option value="28">
+                      28
+                    </option>
+
+                    <option value="29">
+                      29
+                    </option>
+
+                    <option value="30">
+                      30
+                    </option>
+
+                    <option value="31">
+                      31
+                    </option>
+                  </select><select name="date_year" id="date_year">
+                    <option value="">
+                      YYYY
+                    </option>
+
+                    <option value="2006">
+                      2006
+                    </option>
+
+                    <option value="2007" selected="selected">
+                      2007
+                    </option>
+
+                    <option value="2008">
+                      2008
+                    </option>
+
+                    <option value="2009">
+                      2009
+                    </option>
+
+                    <option value="2010">
+                      2010
+                    </option>
+
+                    <option value="2011">
+                      2011
+                    </option>
+
+                    <option value="2012">
+                      2012
+                    </option>
+
+                    <option value="2013">
+                      2013
+                    </option>
+
+                    <option value="2014">
+                      2014
+                    </option>
+
+                    <option value="2015">
+                      2015
+                    </option>
+
+                    <option value="2016">
+                      2016
+                    </option>
+
+                    <option value="2017">
+                      2017
+                    </option>
+                  </select>
+
+                  <div id="goto" style="display:none">
+                  </div><a href="#" onclick=
+                  "openCalendar('dategoto', 'date'); return false;"
+                  title="Select a date"><img src=
+                  "themes/graphics/calendar.png" alt="Calendar"
+                  title="" id="dategoto" name="dategoto" /></a>
+                </td>
+              </tr>
+
+              <tr valign="top">
+                <td width="15%" align="right"><span class=
+                "form-error"><img src=
+                "themes/graphics/required.png"
+                alt="*" title="" /></span>&nbsp;Client</td>
+
+                <td>
+                  <span id='noClients' class="form-error">There are no clients
+                    which you have access to.</span>
+                  <select name="client" id="client" style='display:none' />
+                </td>
+              </tr>
+
+              <tr valign="top">
+                <td width="15%" align="right"><span class=
+                "form-error"><img src=
+                "themes/graphics/required.png"
+                alt="*" title="" /></span>&nbsp;Job Type</td>
+
+                <td>
+                  <span id='noJobTypes' class="form-error">There are no job
+                    types configured.</span>
+                  <select name="jobType" id="jobType" style='display:none' />
+                </td>
+              </tr>
+
+              <tr valign="top">
+                <td width="15%" align="right"><span class=
+                "form-error"><img src=
+                "themes/graphics/required.png"
+                alt="*" title="" /></span>&nbsp;Cost Object</td>
+
+                <td><select name="costObject" id="costObject">
+                  <option value="" selected="selected">
+                    --- No Cost Object ---
+                  </option>
+                </select>
+                </td>
+              </tr>
+
+              <tr valign="top">
+                <td width="15%" align="right"><span class=
+                "form-error"><img src=
+                "themes/graphics/required.png"
+                alt="*" title="" /></span>&nbsp;Hours</td>
+
+                <td><input type="text" size="5" name="hours" id=
+                "hours" value="" /></td>
+              </tr>
+
+              <tr valign="top">
+                <td width="15%" align="right"><span class=
+                "form-error"><img src=
+                "themes/graphics/required.png"
+                alt="*" title="" /></span>&nbsp;Billable?</td>
+
+                <td><select name="billable" id="billable">
+                  <option value="1">
+                    Yes
+                  </option>
+
+                  <option value="0">
+                    No
+                  </option>
+                </select></td>
+              </tr>
+
+              <tr valign="top">
+                <td width="15%" align="right"><span class=
+                "form-error"><img src=
+                "themes/graphics/required.png"
+                alt="*" title="" /></span>&nbsp;Description</td>
+
+                <td>
+                <textarea id="description" name="description" cols=
+                "40" rows="2">
+</textarea></td>
+              </tr>
+
+              <tr valign="top">
+                <td width="15%" align="right">Additional Notes</td>
+
+                <td>
+                <textarea id="note" name="note" cols="40" rows="2">
+</textarea></td>
+              </tr>
+            </table>
+          </div>
+
+          <div class="control">
+            <input class="button" name="submitbutton" type="submit"
+                onclick="javascript:recordTime()" value="Save" />
+            <input class="button" name="resetbutton" type="reset"
+                onclick="javascript:resetTimeForm()" value="Reset Form" />
+            <input class="button" name="resyncbutton" type="submit"
+                onclick="javascript:forceRefresh()" value="Refresh Server Data" />
+          <img id="spinner-front" src="themes/graphics/spinner-transparent.gif" />
+          </div>
+        </div>
+      <!-- /form -->
+    </div>
+    <div id='infoButton' style='position: absolute; top: 5px; left:400px'></div>
+    <br />
+  </div>
+
+  <div id="back" style='display: none'>
+    <!-- All of the Apple widgets seem to break with W3C standard by not making
+      -  form elements part of an active form.  This has the side-effect of 
+      -  preventing most enter-key presses from submitting the form to a
+      -  non-existant target (which breaks the JS "magic").  Not my ideal way
+      -  to do this, but I guess it works, at least for now.
+      -  2007-11-10 bklang
+      -->
+    <!-- form id='configuration_form' -->
+      URL to Horde's rpc.php (for XML-RPC):
+      <input type="text" name="horde_url" id='horde_url' /><br />
+      Horde Username:
+      <input type="text" name="horde_username" id='horde_username' /><br />
+      Horde Password:
+      <input type="password" name="horde_password" id='horde_password' /><br />
+      <ul id="prefMsgBox" class="notices"></ul>
+      <div id="doneButton"></div>
+      <input type='submit' value='Refresh Server Data' onClick='javascript:forceRefresh();' />
+      <img id="spinner-back" src="themes/graphics/spinner-transparent.gif" />
+    <!-- /form -->
+  </div>
+</body>
+</html>
diff --git a/hermes/scripts/Sandals.wdgt/lib/Sandals.js b/hermes/scripts/Sandals.wdgt/lib/Sandals.js
new file mode 100644 (file)
index 0000000..d7c5e56
--- /dev/null
@@ -0,0 +1,794 @@
+/**
+ * $Horde: hermes/scripts/Sandals.wdgt/lib/Sandals.js,v 1.5 2008/03/11 22:57:38 bklang Exp $
+ * This file is part of Sandals.wdgt, distributed with the Hermes application.
+ * Hermes is part of the Horde Project (http://www.horde.org/)
+ *
+ * Sandals is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Sandals is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Horde; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * Copyright 2006-2008 Ben Klang <ben@alkaloid.net>
+ *
+ * serialize() based on phpSerialize
+ * phpSerialize() and getObjectClass() are courtesy of:
+ * http://magnetiq.com/2006/07/30/php-style-serialization-of-javascript-objects/
+ *
+ * unserialize() based on PHP_Unserialize
+ * PHP_Unserialize (C) 2005 Richard Heyes <http://www.phpguru.org/>
+ * 
+ * @author Ben Klang <ben@alkaloid.net>
+ */
+
+// Global variables
+var gDoneButton; 
+var gInfoButton; 
+var currentArea = false;
+// XML-RPC object
+var hermes;
+// Form data arrays
+var clients;
+var jobTypes;
+var costObjects;
+// Boolean: keep track of whether we need to force a refresh of the server data
+var prefsChanged;
+
+// Connection details for Horde instance
+var url = '';
+var username = '';
+var password = '';
+
+
+if (window.widget) {
+    //widget.onremove = onremove;
+    //widget.onhide = onhide;
+    widget.onshow = onshow;
+}
+//
+//function onremove()
+//{
+//    alert("Remove");
+//}
+//
+//function onhide()
+//{
+//    alert("Hide");
+//}
+
+/**
+ * Seed the UI with the current date each time Dashboard is shown
+ */
+function onshow()
+{
+    var date = new Date();
+    document.getElementById('date_month').value = date.getMonth() + 1;
+    document.getElementById('date_day').value = date.getDate();
+    document.getElementById('date_year').value = date.getFullYear();
+    document.getElementById('hours').focus();
+}
+
+/**
+ * Create the glowing "i" button to access the back panel
+ */
+function setup()
+{
+    try {
+        var front = document.getElementById('front');
+        var infoButton = document.getElementById('infoButton');
+        var back = document.getElementById('back');
+        var doneButton = document.getElementById('doneButton');
+        // Create the Info and Done buttons
+        gDoneButton = new AppleGlassButton(doneButton, "Done", hidePrefs);
+        gInfoButton = new AppleInfoButton(infoButton, front, "black", "white", showPrefs); 
+        onshow();
+        
+        // Try to load saved preferences
+        if (window.widget) {
+            url = widget.preferenceForKey(widget.identifier + '@url') || '';
+            username = widget.preferenceForKey(widget.identifier + '@username') || '';
+
+            // Try to use Keychain to read the password information.
+            // This seems to work on OS X 10.5 only
+            if (widget.KeychainPlugIn && url != '') {
+                urlparts = splitURL(url);
+                password = KeychainPlugIn.getPassword(username,
+                                                      urlparts.serverName,
+                                                      urlparts.serverPath) || '';
+            } else {
+                // Fall back to storing the password in preferences
+                password = widget.preferenceForKey(widget.identifier + '@password') || '';
+            }
+            document.getElementById('horde_url').value = url;
+            document.getElementById('horde_username').value = username;
+            document.getElementById('horde_password').value = password;
+
+        }
+        // Disable the spinners
+        document.getElementById('spinner-front').style.display = 'none';
+        document.getElementById('spinner-back').style.display = 'none';
+    } catch(e) {
+        // Disable the spinners
+        document.getElementById('spinner-front').style.display = 'none';
+        document.getElementById('spinner-back').style.display = 'none';
+        showMsg(e)
+    }
+
+}
+
+/**
+ * Flip to the back panel and show configuration options
+ */
+function showPrefs() 
+{ 
+    var front = document.getElementById("front"); 
+    var back = document.getElementById("back"); 
+
+    if (window.widget) {
+        widget.prepareForTransition("ToBack"); 
+    }
+
+    front.style.display="none"; 
+    back.style.display="block"; 
+
+    if (window.widget) {
+        setTimeout ('widget.performTransition();', 0); 
+    }
+}
+
+/**
+ * This function will be called to flip back to the front screen.
+ * First check the user-supplied data.  Then attempt to load the
+ * server data from Horde.  If no errors occur flip back to the
+ * front UI. On error, show the message and do not flip back.
+ */
+function hidePrefs()
+{
+    // Clear any error messages
+    showMsg();
+
+    // Enable the spinners
+    document.getElementById('spinner-front').style.display = 'inline';
+    document.getElementById('spinner-back').style.display = 'inline';
+    
+    try {
+        validateAndStore();
+        // If our configuration changed refresh the server data
+        if (prefsChanged) {
+            refreshServerData();
+        }
+    } catch(e) {
+        // Disable the spinners and show the error
+        document.getElementById('spinner-front').style.display = 'none';
+        document.getElementById('spinner-back').style.display = 'none';
+        showMsg(e);
+        return false;
+    }
+
+    var front = document.getElementById("front");
+    var back = document.getElementById("back");
+
+    // Disable the spinners
+    document.getElementById('spinner-front').style.display = 'none';
+    document.getElementById('spinner-back').style.display = 'none';
+
+    // Flip back to the front
+    if (window.widget) {
+        widget.prepareForTransition("ToFront");
+    }
+
+    front.style.display="block";
+    back.style.display="none";
+
+    if (window.widget) {
+        setTimeout('widget.performTransition();', 0);
+    }
+
+}
+
+/**
+ * Check the URL, username, and password supplied by the user.
+ * Store this information in the widget preferences.
+ * Also determine whether the preferences have changed
+ * since the last time they were stored.  This may indicate that
+ * a refresh of server data is required.
+ * Finally, construct a URL from that data and configure the
+ * XML-RPC handler.
+ */
+function validateAndStore()
+{
+    url = document.getElementById("horde_url").value || '';
+    username = document.getElementById("horde_username").value || '';
+    password = document.getElementById("horde_password").value || '';
+    if (url == '' || username == '' || password == '') {
+        //throw('You must supply the full URL to Horde\'s rpc.php as well as a valid username and password to continue.');    
+        throw('Horde connection not yet configured.');
+    }
+
+    // Make sure the url has a schema.  Default to non-SSL
+    if (url.indexOf('http') != 0) {
+        url = "http://"+url;
+    }
+
+    // Determine if the Horde instance (url + username + password) changed
+    prefsChanged = true;
+    if (window.widget) {
+        var oldurl = widget.preferenceForKey(widget.identifier + '@url');
+        var oldusername = widget.preferenceForKey(widget.identifier + '@username');
+        var oldpassword = widget.preferenceForKey(widget.identifier + '@password');
+
+        if (url == oldurl &&
+            username == oldusername &&
+            password == oldpassword) {
+
+            // Config data has not changed.
+            prefsChanged = false;
+        }
+
+        // Store changed preferences
+        if (prefsChanged) {
+            widget.setPreferenceForKey(url, widget.identifier + "@url");
+            widget.setPreferenceForKey(username, widget.identifier + "@username");
+            // Try to use Keychain to store the password.
+            // This seems to work on OS X 10.5 only
+            if (widget.KeychainPlugIn) {
+                urlparts = splitURL(url);
+                KeychainPlugIn.addPassword(password, username, urlparts.serverName,
+                                                   urlparts.serverPath);
+            } else {
+                // Fall back to preference storage
+                widget.setPreferenceForKey(password, widget.identifier + "@password");
+            }
+        }
+    }
+    
+    // Will hold fully-qualified URL including username and password
+    var fqurl = '';
+    // SSL flag
+    var ssl = false;
+    // Strip off 'http://' if it is there
+    if (url.indexOf('http://') == 0) {
+        fqurl = url.substr(7);
+        ssl = false;
+    } else if (url.indexOf('https://') == 0) {
+        fqurl = url.substr(8);
+        ssl = true;
+    } else {
+        fqurl = url;
+    }
+
+    var creds = '';
+    // Build the URL
+    if (username) {
+        creds += escape(username);
+        if (password)
+            creds += ':' + escape(password);
+        creds += '@';
+    }
+
+    if (ssl) {
+        fqurl = 'https://' + creds + fqurl;
+    } else {
+        fqurl = 'http://' + creds + fqurl;
+    }
+
+    hermes = XMLRPC.getService(fqurl);
+    hermes.add("system.listMethods", "listMethods");
+    hermes.add("time.listJobTypes", "listJobTypes");
+    hermes.add("time.listClients", "listClients");
+    hermes.add("time.listCostObjects", "listCostObjects");
+    hermes.add("time.recordTime", "recordTime");
+    XMLRPC.onerror = function(e){ showMsg(e); }
+
+    return true;
+}
+
+/**
+ * Force a refresh of the server data.  This is initiated by a
+ * button press on the back panel.
+ */
+function forceRefresh()
+{
+    // Clear any old errors
+    showMsg();
+    try {
+        validateAndStore();
+        refreshServerData();   
+    } catch(e) { showMsg(e); }
+}
+
+/**
+ * Refresh the current cache of server data, including the list of
+ * clients, job types, and cost objects.  This method assumes
+ * a valid XML-RPC object has been instantiated.
+ * TODO: Spinner added but since this method is called synchronously it is
+ * never visible.  Fix vcXMLRPC to use XMLHttpRequest and the onreadystatechange
+ * callback.
+ */
+function refreshServerData()
+{
+    try {
+        clients = hermes.listClients();
+        jobTypes = hermes.listJobTypes();
+        // TODO: use listMethods and enumerate all Horde apps providing a
+        // listCostObjects method.  For now, we'll just use the deliverables
+        // configured in Hermes.
+        // FIXME: Iterate over clients and call once for each client.  Yes,
+        // this is heavy-handed (and slow!) but it appears to be the only way
+        // to get a list of deliverables valid for each client.  The
+        // alternative would be to change the information returned by the
+        // listCostObjects API.  Maybe for Horde 4?
+        costObjects = hermes.listCostObjects(new Array());
+    } catch(e) {
+        // vcXMLRPC doesn't do a good job of catching 401 so we do it manually.
+        // 401 (Unauthorized) is the proper error code when a username or
+        // password is incorrect.  This is indeed what you get when tested in
+        // Safari.  For some reason, when running under Dashboard, the status
+        // is set to -1012.  My best guess is related to teh fact that
+        // 1012 is the error code for XML_RNGP_DEFINE_EMPTY in libxml.
+        // If that is not where the 1012 comes from, I have no idea.
+        if (e.http_status == 401 || e.http_status == -1012) {
+            throw "Invalid username or password.";
+        }
+        throw e;
+    }
+
+    // TODO: Cache this information in Dashboard prefs
+    //if (window.widget) {
+    //    widget.setPreferenceForKey(serialize(clients),
+    //                               widget.identifier + "@clients");
+    //    widget.setPreferenceForKey(serialize(jobTypes),
+    //                               widget.identifier + "@jobTypes");
+    //}
+    
+    //
+    // Refresh the client list drop-down
+    //
+    var noClientsWarning = document.getElementById('noClients');
+    var clientSelector = document.getElementById('client');
+
+    // Since the select-boxes are slightly taller than plain text, enlarge the
+    // window slightly so as not to cut off the bottom edge of the buttons.
+    if (window.widget && !clientSelector.firstChild) {
+        window.resizeBy(0, 4);
+    }
+
+    while (clientSelector.firstChild) {
+        clientSelector.removeChild(clientSelector.firstChild);
+    }
+    for (client in clients) {
+        // FIXME: This is a hack
+        // Ignore the inherited toXMLRPC from vcXMLRPC
+        if (client == 'toXMLRPC') {
+            continue;
+        }
+        element = document.createElement('option');
+        element.value = client;
+        element.innerHTML = clients[client];
+        clientSelector.appendChild(element);
+    }
+    // If we have a valid list, show the selectbox.
+    if (clientSelector.firstChild) {
+        noClientsWarning.style.display = 'none';
+        clientSelector.style.display = '';
+    } else {
+        // ...otherwise show the "no clients" error
+        noClientsWarning.style.display = 'inline';
+        clientSelector.style.display = 'none';
+    }
+
+    //
+    // Refresh the job types drop-down
+    //
+    var noJobTypesWarning = document.getElementById('noJobTypes');
+    var jobTypeSelector = document.getElementById('jobType');
+    while (jobTypeSelector.firstChild) {
+        jobTypeSelector.removeChild(jobTypeSelector.firstChild);
+    }
+    while (jobType = jobTypes.pop()) {
+        element = document.createElement('option');
+        element.value = jobType.id;
+        element.innerHTML = jobType.name;
+        jobTypeSelector.appendChild(element);
+    }
+    // If we have a valid list, show the selectbox.
+    if (jobTypeSelector.firstChild) {
+        noJobTypesWarning.style.display = 'none';
+        jobTypeSelector.style.display = '';
+    } else {
+        // ...otherwise show the "no jobTypess" error
+        noJobTypesWarning.style.display = 'inline';
+        jobTypeSelector.style.display = 'none';
+    }
+
+    //
+    // Refresh the cost objects drop-down
+    //
+    var costObjectSelector = document.getElementById('costObject');
+    while (costObjectSelector.firstChild) {
+        costObjectSelector.removeChild(costObjectSelector.firstChild);
+    }
+    // Seed the default value
+    element = document.createElement('option');
+    element.value = '';
+    element.innerHTML = '--- No Cost Object ---';
+    costObjectSelector.appendChild(element);
+    while (costObjectCategory = costObjects.pop()) {
+        // Create the category header
+        element = document.createElement('option');
+        element.value = '';
+        element.innerHTML = "---" + costObjectCategory.category + "---";
+        costObjectSelector.appendChild(element);
+        while (costObject = costObjectCategory.objects.pop()) {
+            if (costObject.active) {
+                element = document.createElement('option');
+                element.value = costObject.id;
+                element.innerHTML = costObject.name;
+                costObjectSelector.appendChild(element);
+            }
+        }
+    }
+}
+
+function recordTime()
+{
+    try {
+        // date will be of the form YYYYMMDD for passing to 
+        // the Horde_Date constructor
+        var date = '';
+        date += document.getElementById('date_year').value;
+        var month = document.getElementById('date_month').value;
+        // zero-pad single-digit values
+        if (month < 10) {
+            date += '0';
+        }
+        date += month;
+        var mday = document.getElementById('date_day').value;
+        if (mday < 10) {
+            date += '0';
+        }
+        date += mday;
+        
+        var client = document.getElementById('client').value;
+        var jobType = document.getElementById('jobType').value;
+        var costObject = document.getElementById('costObject').value;
+        var hours = document.getElementById('hours').value;
+        var billable = document.getElementById('billable').value;
+        var description = document.getElementById('description').value;
+        var note = document.getElementById('note').value;
+        
+        hermes.recordTime(date, client, jobType, costObject, hours,
+                          billable, description, note);
+    } catch(e) {
+        showMsg(e)
+    }
+}
+
+function showMsg(message, level)
+{
+    if (message && !level) {
+        level = 'warning';
+    }
+
+    var prefMsgBox = document.getElementById('prefMsgBox');
+    var msgBox = document.getElementById('msgBox');
+    if (!message) {
+        // Clear the message box
+        // Assume the msgBox and prefMsgBox have the same number of children.
+        // This also allows consistent window resizing
+        while (msgBox.hasChildNodes()) {
+            prefMsgBox.removeChild(prefMsgBox.firstChild);
+            msgBox.removeChild(msgBox.firstChild);
+            if (window.widget) {
+                window.resizeBy(0, -27);
+            }
+        }
+    } else {
+        // Create an li entry for the error message
+        // and display the icon with the message
+        if (window.widget) {
+            window.resizeBy(0, 27);
+        }
+        msgbox = document.createElement('li');
+        msgico = document.createElement('img');
+        msgico.setAttribute('src', 'themes/graphics/alerts/'+level+'.png');
+        prefMsgBox.appendChild(msgbox);
+        prefMsgBox.lastChild.appendChild(msgico);
+        prefMsgBox.lastChild.appendChild(document.createTextNode(message));
+
+        msgbox = document.createElement('li');
+        msgico = document.createElement('img');
+        msgico.setAttribute('src', 'themes/graphics/alerts/'+level+'.png');
+        msgBox.appendChild(msgbox);
+        msgBox.lastChild.appendChild(msgico);
+        msgBox.lastChild.appendChild(document.createTextNode(message));
+    }
+    return true;
+}
+
+function showArea(area)
+{
+    try {
+        currentArea.style['display'] = 'hidden';
+    } catch(e) {}
+    currentArea = document.getElementById(area);
+    currentArea.style['display'] = 'block';
+}
+
+/**
+ * Split a URL into server name and path
+ */
+function splitURL(url)
+{
+    if (!url) {
+        return false;
+    }
+    var m = url.match(/\w+:\/\/([^\/]*)\/(.*)/);
+    if (!m.length || m.length != 3) {
+        return false;
+    }
+    return { serverName: m[1], serverPath: m[2] };
+}
+
+function resetTimeForm()
+{
+    document.getElementById('hours').value='';
+    document.getElementById('description').value='';    
+    document.getElementById('note').value='';
+    showMsg();
+}
+
+/**
+ * Returns the class name of the argument or 'GenericObject' if
+ * it the object provenance cannot be determined.
+ */
+function getObjectClass(obj)
+{
+    if (obj && obj.constructor && obj.constructor.toString)
+    {
+        var arr = obj.constructor.toString().match(
+            /function\s*(\w+)/);
+
+        if (arr && arr.length == 2)
+        {
+            return arr[1];
+        }
+    }
+
+    //return undefined;
+    return 'GenericObject';
+}
+
+/**
+ * Serializes the given argument, PHP-style.
+ *
+ * The type mapping is as follows:
+ *
+ * JavaScript Type    PHP Type
+ * ---------------    --------
+ * Number             Integer or Decimal
+ * String             String
+ * Boolean            Boolean
+ * Array              Array
+ * Object             Object
+ * undefined          Null
+ *
+ * The special JavaScript object null also becomes PHP Null.
+ * This function may not handle associative arrays or array
+ * objects with additional properties well.
+ */
+function serialize(val)
+{
+    switch (typeof(val))
+    {
+    case "number":
+        return (Math.floor(val) == val ? "i" : "d") + ":" +
+            val + ";";
+    case "string":
+        return "s:" + val.length + ":\"" + val + "\";";
+    case "boolean":
+        return "b:" + (val ? "1" : "0") + ";";
+    case "object":
+        if (val == null)
+        {
+            return "N;";
+        }
+        else if ("length" in val)
+        {
+            var idxobj = { idx: -1 };
+
+            return "a:" + val.length + ":{" + val.map(
+                function (item)
+                {
+                    this.idx++;
+
+                    var ser = serialize(item);
+
+                    return ser ?
+                        serialize(this.idx) + ser :
+                        false;
+                }, idxobj).filter(
+                function (item)
+                {
+                    return item;
+                }).join("") + "}";
+        }
+        else
+        {
+            var class_name = getObjectClass(val);
+
+            if (class_name == undefined)
+            {
+                return false;
+            }
+
+            var props = new Array();
+
+            for (var prop in val)
+            {
+                var ser = serialize(val[prop]);
+
+                if (ser)
+                {
+                    props.push(serialize(prop) + ser);
+                }
+            }
+            return "O:" + class_name.length + ":\"" +
+                class_name + "\":" + props.length + ":{" +
+                props.join("") + "}";
+        }
+    case "undefined":
+        return "N;";
+    }
+
+    return false;
+}
+
+/**
+ * Unserializes a PHP serialized data type. Currently handles:
+ *  o Strings
+ *  o Integers
+ *  o Doubles
+ *  o Arrays
+ *  o Booleans
+ *  o NULL
+ *  o Objects
+ * 
+ * @param  string input The serialized PHP data
+ * @return mixed        The resulting datatype
+ */
+function unserialize(input)
+{
+    var result = PHP_Unserialize_(input);
+    alert(result.length);
+    return result[0];
+}
+
+
+/**
+ * Function which performs the actual unserializing
+ *
+ * @param string input Input to parse
+ */
+function PHP_Unserialize_(input)
+{
+    var length = 0;
+    
+    switch (input.charAt(0)) {
+        /**
+        * Array
+        */
+        case 'a':
+            length = PHP_Unserialize_GetLength(input);
+            input  = input.substr(String(length).length + 4);
+
+            var arr   = new Array();
+            var key   = null;
+            var value = null;
+
+            for (var i=0; i<length; ++i) {
+                key   = PHP_Unserialize_(input);
+                input = key[1];
+
+                value = PHP_Unserialize_(input);
+                input = value[1];
+
+                arr[key[0]] = value[0];
+            }
+
+            input = input.substr(1);
+            return [arr, input];
+            break;
+        
+        /**
+        * Objects
+        */
+        case 'O':
+            length = PHP_Unserialize_GetLength(input);
+            var classname = String(input.substr(String(length).length + 4, length));
+            
+            input  = input.substr(String(length).length + 6 + length);
+            var numProperties = Number(input.substring(0, input.indexOf(':')))
+            input = input.substr(String(numProperties).length + 2);
+
+            var obj      = new Object();
+            var property = null;
+            var value    = null;
+
+            for (var i=0; i<numProperties; ++i) {
+                key   = PHP_Unserialize_(input);
+                input = key[1];
+                
+                // Handle private/protected
+                key[0] = key[0].replace(new RegExp('^\x00' + classname + '\x00'), '');
+                key[0] = key[0].replace(new RegExp('^\x00\\*\x00'), '');
+
+                value = PHP_Unserialize_(input);
+                input = value[1];
+
+                obj[key[0]] = value[0];
+            }
+
+            input = input.substr(1);
+            return [obj, input];
+            break;
+
+        // Strings
+        case 's':
+            length = PHP_Unserialize_GetLength(input);
+            return [String(input.substr(String(length).length + 4, length)), input.substr(String(length).length + 6 + length)];
+            break;
+
+        // Integers and doubles
+        case 'i':
+        case 'd':
+            var num = Number(input.substring(2, input.indexOf(';')));
+            return [num, input.substr(String(num).length + 3)];
+            break;
+        
+        // Booleans
+        case 'b':
+            var bool = (input.substr(2, 1) == 1);
+            return [bool, input.substr(4)];
+            break;
+        
+        // Null
+        case 'N':
+            return [null, input.substr(2)];
+            break;
+
+        // Unsupported
+        case 'o':
+        case 'r':
+        case 'C':
+        case 'R':
+        case 'U':
+            showMsg('Unsupported PHP data type found!');
+
+        // Error
+        default:
+            return [null, null];
+            break;
+    }
+}
+
+
+/**
+ * Returns length of strings/arrays etc
+ *
+ * @param string input Input to parse
+ */
+function PHP_Unserialize_GetLength(input)
+{
+    input = input.substring(2);
+    var length = Number(input.substr(0, input.indexOf(':')));
+    return length;
+}
diff --git a/hermes/scripts/Sandals.wdgt/lib/horde.js b/hermes/scripts/Sandals.wdgt/lib/horde.js
new file mode 100644 (file)
index 0000000..876e240
--- /dev/null
@@ -0,0 +1,267 @@
+/**
+ * General Horde UI effects javascript.
+ *
+ * $Horde: hermes/scripts/Sandals.wdgt/lib/horde.js,v 1.2 2008/03/08 06:52:38 bklang Exp $
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ */
+
+var ToolTips = {
+    current: null,
+    timeout: null,
+    element: null,
+
+    attachBehavior: function()
+    {
+        links = document.getElementsByTagName('a');
+        for (i = 0; i < links.length; i++) {
+            if (links[i].title) {
+                links[i].setAttribute('nicetitle', links[i].title);
+                links[i].removeAttribute('title');
+
+                addEvent(links[i], 'mouseover', ToolTips.onMouseover);
+                addEvent(links[i], 'mouseout', ToolTips.out);
+                addEvent(links[i], 'focus', ToolTips.onFocus);
+                addEvent(links[i], 'blur', ToolTips.out);
+            }
+        }
+    },
+
+    onMouseover: function(event)
+    {
+        if (typeof ToolTips == 'undefined') {
+            return;
+        }
+
+        if (ToolTips.timeout) {
+            clearTimeout(ToolTips.timeout);
+        }
+
+        if (event.srcElement) {
+            ToolTips.element = event.srcElement;
+        } else if (event.target) {
+            ToolTips.element = event.target;
+        }
+
+        var pos = mousePos(event);
+        ToolTips.timeout = setTimeout(function() { ToolTips.show(pos); }, 300)
+    },
+
+    onFocus: function(event)
+    {
+        if (typeof ToolTips == 'undefined') {
+            return;
+        }
+
+        if (ToolTips.timeout) {
+            clearTimeout(ToolTips.timeout);
+        }
+
+        if (event.srcElement) {
+            ToolTips.element = event.srcElement;
+        } else if (event.target) {
+            ToolTips.element = event.target;
+        }
+
+        var pos = eltPos(ToolTips.element);
+        ToolTips.timeout = setTimeout(function() { ToolTips.show(pos); }, 300)
+    },
+
+    out: function()
+    {
+        if (typeof ToolTips == 'undefined') {
+            return;
+        }
+
+        if (ToolTips.timeout) {
+            clearTimeout(ToolTips.timeout);
+        }
+
+        if (ToolTips.current) {
+            document.getElementsByTagName('body')[0].removeChild(ToolTips.current);
+            ToolTips.current = null;
+
+            var iframe = document.getElementById('iframe_tt');
+            if (iframe != null) {
+                iframe.style.display = 'none';
+            }
+        }
+    },
+
+    show: function(pos)
+    {
+        try {
+            if (ToolTips.current) {
+                ToolTips.out();
+            }
+
+            var link = ToolTips.element;
+            while (!link.getAttribute('nicetitle') && link.nodeName.toLowerCase() != 'body') {
+                link = link.parentNode;
+            }
+            var nicetitle = link.getAttribute('nicetitle');
+            if (!nicetitle) {
+                return;
+            }
+
+            var d = document.createElement('div');
+            d.className = 'nicetitle';
+            d.innerHTML = nicetitle;
+
+            var STD_WIDTH = 100;
+            var MAX_WIDTH = 600;
+            if (window.innerWidth) {
+                MAX_WIDTH = Math.min(MAX_WIDTH, window.innerWidth - 20);
+            }
+            if (document.body && document.body.scrollWidth) {
+                MAX_WIDTH = Math.min(MAX_WIDTH, document.body.scrollWidth - 20);
+            }
+
+            var nicetitle_length = 0;
+            var lines = nicetitle.replace(/<br ?\/>/g, "\n").split("\n");
+            for (var i = 0; i < lines.length; ++i) {
+                nicetitle_length = Math.max(nicetitle_length, lines[i].length);
+            }
+
+            var h_pixels = nicetitle_length * 7;
+            var t_pixels = nicetitle_length * 10;
+
+            var w, h;
+            if (h_pixels > STD_WIDTH) {
+                w = h_pixels;
+            } else if (STD_WIDTH > t_pixels) {
+                w = t_pixels;
+            } else {
+                w = STD_WIDTH;
+            }
+
+            // Make sure all of the tooltip is visible
+            var left = pos[0] + 20,
+                innerWidth = window.innerWidth || document.documentElement.clientWidth || document.body.offsetWidth,
+                pageXOffset = window.pageXOffset || document.documentElement.scrollLeft;
+            if (innerWidth && ((left + w) > (innerWidth + pageXOffset))) {
+                left = innerWidth - w - 40 + pageXOffset;
+            }
+            if (document.body.scrollWidth && ((left + w) > (document.body.scrollWidth + pageXOffset))) {
+                left = document.body.scrollWidth - w - 25 + pageXOffset;
+            }
+
+            d.id = 'toolTip';
+            d.style.left = left + 'px';
+            d.style.width = Math.min(w, MAX_WIDTH) + 'px';
+            d.style.top = (pos[1] + 10) + 'px';
+            d.style.display = '';
+
+            document.getElementsByTagName('body')[0].appendChild(d);
+            ToolTips.current = d;
+
+            if (typeof ToolTips_Option_Windowed_Controls != 'undefined') {
+                var iframe = document.getElementById('iframe_tt');
+                if (iframe == null) {
+                    iframe = document.createElement('<iframe src="javascript:false;" name="iframe_tt" id="iframe_tt" scrolling="no" frameborder="0" style="position:absolute;top:0;left:0;display:none"></iframe>');
+                    document.getElementsByTagName('body')[0].appendChild(iframe);
+                }
+                iframe.style.width = d.offsetWidth;
+                iframe.style.height = d.offsetHeight;
+                iframe.style.top = d.style.top;
+                iframe.style.left = d.style.left;
+                iframe.style.position = 'absolute';
+                iframe.style.display = 'block';
+                d.style.zIndex = 100;
+                iframe.style.zIndex = 99;
+            }
+        } catch (e) {}
+    }
+
+};
+
+/**
+ * Return the [x,y] position of the mouse.
+ */
+function mousePos(event)
+{
+    return [event.pageX || (event.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft)),
+            event.pageY || (event.clientY + (document.documentElement.scrollTop || document.body.scrollTop))];
+}
+/**
+ * Return the [x,y] position of an element.
+ */
+function eltPos(elt)
+{
+    if (elt.offsetParent) {
+        for (posX = 0, posY = 0; elt.offsetParent; elt = elt.offsetParent) {
+            posX += elt.offsetLeft;
+            posY += elt.offsetTop;
+        }
+        return [posX, posY];
+    } else {
+        return [elt.x, elt.y];
+    }
+}
+
+/**
+ * Add an event listener as long as the browser supports it. Different
+ * browsers still handle these events slightly differently; in
+ * particular avoid using "this" in event functions.
+ *
+ * @author Scott Andrew
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ */
+function addEvent(obj, evType, fn)
+{
+    if (obj.addEventListener) {
+        obj.addEventListener(evType, fn, true);
+        return true;
+    } else if (obj.attachEvent) {
+        var r = obj.attachEvent('on' + evType, fn);
+        EventCache.add(obj, evType, fn);
+        return r;
+    } else {
+        return false;
+    }
+}
+
+var EventCache = function()
+{
+    var listEvents = [];
+
+    return {
+        listEvents: listEvents,
+
+        add: function(node, sEventName, fHandler, bCapture)
+        {
+            listEvents.push(arguments);
+        },
+
+        flush: function()
+        {
+            var i, item;
+            for (i = listEvents.length - 1; i >= 0; i = i - 1) {
+                item = listEvents[i];
+
+                if (item[0].removeEventListener) {
+                    item[0].removeEventListener(item[1], item[2], item[3]);
+                };
+
+                /* From this point on we need the event names to be
+                 * prefixed with 'on'. */
+                if (item[1].substring(0, 2) != 'on') {
+                    item[1] = 'on' + item[1];
+                }
+
+                if (item[0].detachEvent) {
+                    item[0].detachEvent(item[1], item[2]);
+                }
+
+                item[0][item[1]] = null;
+            }
+        }
+    };
+}();
+
+if (document.createElement && document.getElementsByTagName) {
+    addEvent(window, 'load', ToolTips.attachBehavior);
+    addEvent(window, 'unload', ToolTips.out);
+    addEvent(window, 'unload', EventCache.flush);
+}
diff --git a/hermes/scripts/Sandals.wdgt/lib/open_calendar.js b/hermes/scripts/Sandals.wdgt/lib/open_calendar.js
new file mode 100644 (file)
index 0000000..9f7223a
--- /dev/null
@@ -0,0 +1,481 @@
+/**
+ * Horde Calendar javascript widget and utility functions.
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ * $Horde: hermes/scripts/Sandals.wdgt/lib/open_calendar.js,v 1.3 2009/01/06 17:50:10 jan Exp $
+ */
+
+/**
+ * Backwards-compatible function that wraps access to Horde_Calendar
+ */
+function openCalendar(imgId, target, callback)
+{
+    var date = new Date();
+    var y, m, d;
+
+    if (document.getElementById(target + '[year]')) {
+        y = document.getElementById(target + '[year]').value;
+        m = document.getElementById(target + '[month]').value;
+        d = document.getElementById(target + '[day]').value;
+    } else if (document.getElementById(target + '_year')) {
+        y = document.getElementById(target + '_year').value;
+        m = document.getElementById(target + '_month').value;
+        d = document.getElementById(target + '_day').value;
+    }
+
+    if (y && m && d) {
+        date = new Date(y, m - 1, d);
+    }
+
+    Horde_Calendar.openDate = date.getTime();
+    Horde_Calendar.draw(Horde_Calendar.openDate, imgId, target, callback);
+}
+
+/**
+ * Configuration settings for Horde_Calendar.
+ */
+var Horde_Calendar_Vars = {
+    firstDayOfWeek: 1
+};
+
+/**
+ * Translations for strings used in Horde_Calendar.
+ */
+var Horde_Calendar_Text = {
+
+    /**
+     * Days of the week
+     */
+    weekdays: [
+        'Su',
+        'Mo',
+        'Tu',
+        'We',
+        'Th',
+        'Fr',
+        'Sa'
+    ],
+
+    /**
+     * Month names
+     */
+    months: [
+        'January',
+        'February',
+        'March',
+        'April',
+        'May',
+        'June',
+        'July',
+        'August',
+        'September',
+        'October',
+        'November',
+        'December'
+    ]
+
+};
+
+/**
+ * Main Horde_Calendar object for rendering javascript calendars.
+ */
+var Horde_Calendar = {
+
+    date: null,
+    target: null,
+    openDate: null,
+
+    /**
+     * Days in the month (month is a zero-indexed javascript month)
+     */
+    daysInMonth: function(month, year)
+    {
+        switch (month) {
+        case 3:
+        case 5:
+        case 8:
+        case 10:
+            return 30;
+        break;
+
+        case 1:
+            if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) {
+                return 29;
+            } else {
+                return 28;
+            }
+            break;
+
+        default:
+            return 31;
+            break;
+        }
+    },
+
+    weeksInMonth: function(month, year)
+    {
+        var firstOfMonth = (new Date(year, month, 1)).getDay();
+        var firstWeekDays, weeks;
+        if (Horde_Calendar_Vars.firstDayOfWeek == 1 && firstOfMonth == 0) {
+            firstWeekDays = 7 - firstOfMonth + Horde_Calendar_Vars.firstDayOfWeek;
+            weeks = 1;
+        } else if (Horde_Calendar_Vars.firstDayOfWeek == 0 && firstOfMonth == 6) {
+            firstWeekDays = 7 - firstOfMonth + Horde_Calendar_Vars.firstDayOfWeek;
+            weeks = 1;
+        } else {
+            firstWeekDays = Horde_Calendar_Vars.firstDayOfWeek - firstOfMonth;
+            weeks = 0;
+        }
+        firstWeekDays %= 7;
+        return Math.ceil((Horde_Calendar.daysInMonth(month, year) - firstWeekDays) / 7) + weeks;
+    },
+
+    draw: function(timestamp, imgId, target, callback)
+    {
+        var row, cell, img, link;
+
+        Horde_Calendar.target = target;
+        if (typeof callback != 'function') {
+            callback = new Function(callback);
+        }
+        Horde_Calendar.callback = callback;
+
+        Horde_Calendar.date = new Date(timestamp);
+        var month = Horde_Calendar.date.getMonth();
+        var year = Horde_Calendar.date.getFullYear();
+
+        var startOfView;
+        var firstOfMonth = (new Date(year, month, 1)).getDay();
+        var daysInView = Horde_Calendar.weeksInMonth(month, year) * 7;
+        var daysInMonth = Horde_Calendar.daysInMonth(month, year);
+
+        if (Horde_Calendar_Vars.firstDayOfWeek == 0) {
+            startOfView = 1 - firstOfMonth;
+
+            // We may need to adjust the number of days in the view if
+            // we're starting weeks on Sunday.
+            if (firstOfMonth == 0) {
+                //daysInView -= 7;
+            }
+            var lastOfMonth = new Date(year, month, daysInMonth);
+            lastOfMonth = lastOfMonth.getDay();
+            if (lastOfMonth == 0) {
+                daysInView += 7;
+            }
+        } else {
+            // @TODO Adjust this for days other than Monday.
+            if (firstOfMonth == 0) {
+                startOfView = -5;
+            } else {
+                startOfView = 2 - firstOfMonth;
+            }
+        }
+
+        var div = document.getElementById('goto');
+        if (div.firstChild) {
+            div.removeChild(div.firstChild);
+        }
+
+        var table = document.createElement('TABLE');
+        var thead = document.createElement('THEAD');
+        var tbody = document.createElement('TBODY');
+        table.appendChild(thead);
+        table.appendChild(tbody);
+        table.className = 'calendarPopup';
+        table.cellSpacing = 0;
+
+        // Title bar.
+        row = document.createElement('TR');
+        cell = document.createElement('TD');
+        cell.colSpan = 7;
+        cell.className = 'rightAlign';
+        link = document.createElement('A');
+        link.href = '#';
+        link.onclick = function()
+        {
+            var div = document.getElementById('goto');
+            div.style.display = 'none';
+            if (div.firstChild) {
+                div.removeChild(div.firstChild);
+            }
+            var iefix = document.getElementById('goto_iefix');
+            if (iefix) {
+                iefix.style.display = 'none';
+            }
+            return false;
+        }
+        link.appendChild(document.createTextNode('x'));
+        cell.appendChild(link);
+        row.appendChild(cell);
+        thead.appendChild(row);
+
+        // Year.
+        row = document.createElement('TR');
+        cell = document.createElement('TD');
+        link = document.createElement('A');
+        link.href = '#';
+        link.innerHTML = '&laquo;';
+        link.onclick = function()
+        {
+            newDate = new Date(Horde_Calendar.date.getFullYear() - 1, Horde_Calendar.date.getMonth(), 1);
+            Horde_Calendar.draw(newDate.getTime(), imgId, Horde_Calendar.target, Horde_Calendar.callback);
+            return false;
+        }
+        cell.appendChild(link);
+        row.appendChild(cell);
+
+        cell = document.createElement('TD');
+        cell.colSpan = 5;
+        cell.align = 'center';
+        var y = document.createTextNode(year);
+        cell.appendChild(y);
+        row.appendChild(cell);
+
+        cell = document.createElement('TD');
+        link = document.createElement('A');
+        link.href = '#';
+        link.innerHTML = '&raquo;';
+        link.onclick = function()
+        {
+            newDate = new Date(Horde_Calendar.date.getFullYear() + 1, Horde_Calendar.date.getMonth(), 1);
+            Horde_Calendar.draw(newDate.getTime(), imgId, Horde_Calendar.target, Horde_Calendar.callback);
+            return false;
+        }
+        cell.appendChild(link);
+        cell.className = 'rightAlign';
+        row.appendChild(cell);
+        thead.appendChild(row);
+
+        // Month name.
+        row = document.createElement('TR');
+        cell = document.createElement('TD');
+        link = document.createElement('A');
+        link.href = '#';
+        link.innerHTML = '&laquo;';
+        link.onclick = function()
+        {
+            var newMonth = Horde_Calendar.date.getMonth() - 1;
+            var newYear = Horde_Calendar.date.getFullYear();
+            if (newMonth == -1) {
+                newMonth = 11;
+                newYear -= 1;
+            }
+            newDate = new Date(newYear, newMonth, 1);
+            Horde_Calendar.draw(newDate.getTime(), imgId, Horde_Calendar.target, Horde_Calendar.callback);
+            return false;
+        }
+        cell.appendChild(link);
+        row.appendChild(cell);
+
+        cell = document.createElement('TD');
+        cell.colSpan = 5;
+        cell.align = 'center';
+        var m = document.createTextNode(Horde_Calendar_Text.months[month]);
+        cell.appendChild(m);
+        row.appendChild(cell);
+
+        cell = document.createElement('TD');
+        cell.className = 'rightAlign';
+        link = document.createElement('A');
+        link.href = '#';
+        link.innerHTML = '&raquo;';
+        link.onclick = function()
+        {
+            newDate = new Date(Horde_Calendar.date.getFullYear(), Horde_Calendar.date.getMonth() + 1, 1);
+            Horde_Calendar.draw(newDate.getTime(), imgId, Horde_Calendar.target, Horde_Calendar.callback);
+            return false;
+        }
+        cell.appendChild(link);
+        row.appendChild(cell);
+        thead.appendChild(row);
+
+        // Weekdays.
+        row = document.createElement('TR');
+        for (var i = 0; i < 7; ++i) {
+            cell = document.createElement('TH');
+            weekday = document.createTextNode(Horde_Calendar_Text.weekdays[(i + Horde_Calendar_Vars.firstDayOfWeek) % 7]);
+            cell.appendChild(weekday);
+            row.appendChild(cell);
+        }
+        thead.appendChild(row);
+
+        // Cache today and open date.
+        var today = new Date();
+        var today_year = today.getFullYear();
+        var today_month = today.getMonth();
+        var today_day = today.getDate();
+        var open = new Date(Horde_Calendar.openDate);
+        var open_year = open.getFullYear();
+        var open_month = open.getMonth();
+        var open_day = open.getDate();
+
+        // Rows.
+        var count = 1;
+        for (var i = startOfView, i_max = startOfView + daysInView; i < i_max; ++i) {
+            if (count == 1) {
+                row = document.createElement('TR');
+            }
+
+            cell = document.createElement('TD');
+
+            if (i < 1 || i > daysInMonth) {
+                row.appendChild(cell);
+
+                if (count == 7) {
+                    tbody.appendChild(row);
+                    count = 0;
+                }
+
+                ++count;
+                continue;
+            }
+
+            if (today_year == year &&
+                today_month == month &&
+                today_day == i) {
+                cell.className = 'today';
+            }
+            if (open_year == year &&
+                open_month == month &&
+                open_day == i) {
+                if (cell.className.length) {
+                    cell.className += ' current';
+                } else {
+                    cell.className = 'current';
+                }
+            }
+
+            link = document.createElement('A');
+            cell.appendChild(link);
+
+            link.href = i;
+            link.onclick = Horde_Calendar.day_onclick;
+
+            day = document.createTextNode(i);
+            link.appendChild(day);
+
+            row.appendChild(cell);
+            if (count == 7) {
+                tbody.appendChild(row);
+                count = 0;
+            }
+            ++count;
+        }
+        if (count > 1) {
+            tbody.appendChild(row);
+        }
+
+        div.appendChild(table);
+        div.style.display = '';
+        div.style.position = 'absolute';
+        div.style.visibility = 'visible';
+
+        // Position the popup every time in case of a different input,
+        // window sizing changes, etc.
+        var el = document.getElementById(imgId);
+        var p = Horde_Calendar.getAbsolutePosition(el);
+
+        if (p.x + div.offsetWidth > document.body.offsetWidth) {
+            div.style.left = (document.body.offsetWidth - 10 - div.offsetWidth) + 'px';
+        } else {
+            div.style.left = p.x + 'px';
+        }
+        if (p.y + div.offsetHeight > document.body.offsetHeight) {
+            div.style.top = (document.body.offsetHeight - 10 - div.offsetHeight) + 'px';
+        } else {
+            div.style.top = p.y + 'px';
+        }
+
+        // Browser sniff for IE taken from Prototype.
+        if (!!(window.attachEvent && !window.opera)) {
+            // Fix for IE and select elements.
+            iefix = document.getElementById('goto_iefix');
+            if (!iefix) {
+                iefix = document.createElement('IFRAME');
+                iefix.id = 'goto_iefix';
+                iefix.src = 'javascript:false;';
+                iefix.scrolling = 'no';
+                iefix.frameborder = 0;
+                iefix.style.display = 'none';
+                iefix.style.position = 'absolute';
+                document.body.appendChild(iefix);
+            }
+            iefix.style.width = div.offsetWidth;
+            iefix.style.height = div.offsetHeight;
+            iefix.style.top = div.style.top;
+            iefix.style.left = div.style.left;
+
+            if (div.style.zIndex == '') {
+                div.style.zIndex = 2;
+                iefix.style.zIndex = 1;
+            } else {
+                iefix.style.zIndex = div.style.zIndex - 1;
+            }
+            iefix.style.display = '';
+        }
+    },
+
+    day_onclick: function()
+    {
+        var day = this.href;
+        while (day.indexOf('/') != -1) {
+            day = day.substring(day.indexOf('/') + 1);
+        }
+
+        // BC
+        if (document.getElementById(Horde_Calendar.target + '[year]')) {
+            Horde_Calendar.setSelectValue(document.getElementById(Horde_Calendar.target + '[year]'), Horde_Calendar.date.getFullYear());
+            Horde_Calendar.setSelectValue(document.getElementById(Horde_Calendar.target + '[month]'), Horde_Calendar.date.getMonth() + 1);
+            Horde_Calendar.setSelectValue(document.getElementById(Horde_Calendar.target + '[day]'), day);
+        } else {
+            Horde_Calendar.setSelectValue(document.getElementById(Horde_Calendar.target + '_year'), Horde_Calendar.date.getFullYear());
+            Horde_Calendar.setSelectValue(document.getElementById(Horde_Calendar.target + '_month'), Horde_Calendar.date.getMonth() + 1);
+            Horde_Calendar.setSelectValue(document.getElementById(Horde_Calendar.target + '_day'), day);
+        }
+
+        var div = document.getElementById('goto');
+        div.style.display = 'none';
+        if (div.firstChild) {
+            div.removeChild(div.firstChild);
+        }
+        var iefix = document.getElementById('goto_iefix');
+        if (iefix) {
+            iefix.style.display = 'none';
+        }
+
+        if (Horde_Calendar.callback) {
+            Horde_Calendar.callback();
+        }
+
+        return false;
+    },
+
+    getAbsolutePosition: function(el)
+    {
+        var r = { x: el.offsetLeft, y: el.offsetTop };
+        if (el.offsetParent) {
+            var tmp = Horde_Calendar.getAbsolutePosition(el.offsetParent);
+            r.x += tmp.x;
+            r.y += tmp.y;
+        }
+        return r;
+    },
+
+    setSelectValue: function(select, value)
+    {
+        select.value = value;
+        if (select.value != value) {
+            for (var i = 0; i < select.options.length; ++i) {
+                if (select.options[i].value == value) {
+                    select.selectedIndex = i;
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+};
diff --git a/hermes/scripts/Sandals.wdgt/lib/stripe.js b/hermes/scripts/Sandals.wdgt/lib/stripe.js
new file mode 100644 (file)
index 0000000..3308888
--- /dev/null
@@ -0,0 +1,97 @@
+/**
+ * Javascript code for finding all tables with classname "striped" and
+ * dynamically striping their row colors.
+ *
+ * Copyright 2006-2009 The Horde Project (http://www.horde.org/)
+ *
+ * $Horde: hermes/scripts/Sandals.wdgt/lib/stripe.js,v 1.3 2009/01/06 17:50:10 jan Exp $
+ *
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ * @author Matt Warden <mwarden@gmail.com>
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ */
+
+/* We do everything onload so that the entire document is present
+ * before we start searching it for tables. */
+if (window.addEventListener) {
+    window.addEventListener('load', findStripedElements, false);
+} else if (window.attachEvent) {
+    window.attachEvent('onload', findStripedElements);
+} else if (window.onload != null) {
+    var oldOnLoad = window.onload;
+    window.onload = function(e)
+    {
+        oldOnLoad(e);
+        findStripedElements();
+    };
+} else {
+    window.onload = findStripedElements;
+}
+
+function findStripedElements()
+{
+    if (!document.getElementsByTagName) {
+        return;
+    }
+    var elts = document.getElementsByTagName('*');
+    for (var i = 0; i < elts.length; ++i) {
+        var e = elts[i];
+        if (e.className.indexOf('striped') != -1) {
+            if (e.tagName == 'TABLE') {
+                stripeTable(e);
+            } else {
+                stripeElement(e);
+            }
+        }
+    }
+}
+
+function stripeTable(table)
+{
+    // The flag we'll use to keep track of whether the current row is
+    // odd or even.
+    var even = false;
+
+    // Tables can have more than one tbody element; get all child
+    // tbody tags and interate through them.
+    var tbodies = table.childNodes;
+    for (var c = 0; c < tbodies.length; c++) {
+        if (tbodies[c].tagName == 'TBODY') {
+            var trs = tbodies[c].childNodes;
+            for (var i = 0; i < trs.length; i++) {
+                if (trs[i].tagName == 'TR') {
+                    trs[i].className = trs[i].className.replace(/ ?rowEven ?/, '').replace(/ ?rowOdd ?/, '');
+                    if (trs[i].className) {
+                        trs[i].className += ' ';
+                    }
+                    trs[i].className += even ? 'rowEven' : 'rowOdd';
+
+                    // Flip from odd to even, or vice-versa.
+                    even = !even;
+                }
+            }
+        }
+    }
+}
+
+function stripeElement(parent)
+{
+    // The flag we'll use to keep track of whether the current elt is
+    // odd or even.
+    var even = false;
+
+    // Toggle the classname of any child node that is an element.
+    var children = parent.childNodes;
+    for (var i = 0; i < children.length; i++) {
+        var tag = children[i];
+        if (tag.nodeType && tag.nodeType == 1) {
+            tag.className = tag.className.replace(/ ?rowEven ?/, '').replace(/ ?rowOdd ?/, '');
+            tag.className = tag.className.split(' ').concat([even ? 'rowEven' : 'rowOdd']).join(' ');
+
+            // Flip from odd to even, or vice-versa.
+            even = !even;
+        }
+    }
+}
diff --git a/hermes/scripts/Sandals.wdgt/lib/vcXMLRPC.js b/hermes/scripts/Sandals.wdgt/lib/vcXMLRPC.js
new file mode 100644 (file)
index 0000000..4c2d1a2
--- /dev/null
@@ -0,0 +1,640 @@
+/**
+ * This file is distributed as a component of Sandals.wdgt
+ * Sandals is a part of the Hermes application of the Horde Project
+ * The Horde Project (http://www.horde.org/)
+ * $Horde: hermes/scripts/Sandals.wdgt/lib/vcXMLRPC.js,v 1.3 2009/01/06 17:50:10 jan Exp $
+ *
+ *    Copyright 2000, 2001, 2002  Virtual Cowboys info@virtualcowboys.nl
+ *             
+ *             Author: Ruben Daniels <ruben@virtualcowboys.nl>
+ *             Version: 0.91
+ *             Date: 29-08-2001
+ *             Site: www.vcdn.org/Public/XMLRPC/
+ *
+ *    This program is free software; you can redistribute it and/or modify
+ *    it under the terms of the GNU General Public License as published by
+ *    the Free Software Foundation; either version 2 of the License, or
+ *    (at your option) any later version.
+ *
+ *    This program is distributed in the hope that it will be useful,
+ *    but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *    GNU General Public License for more details.
+ *
+ *    You should have received a copy of the GNU General Public License
+ *    along with this program; if not, write to the Free Software
+ *    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @author Ruben Daniels <ruben@virtualcowboys.nl>
+ */
+
+
+Object.prototype.toXMLRPC = function(){
+       var wo = this.valueOf();
+       
+       if(wo.toXMLRPC == this.toXMLRPC){
+               retstr = "<struct>";
+               
+               for(prop in this){
+                       if(typeof wo[prop] != "function"){
+                               retstr += "<member><name>" + prop + "</name><value>" + XMLRPC.getXML(wo[prop]) + "</value></member>";
+                       }
+               }
+               retstr += "</struct>";
+               
+               return retstr;
+       }
+       else{
+               return wo.toXMLRPC();
+       }
+}
+
+String.prototype.toXMLRPC = function(){
+       //<![CDATA[***your text here***]]>
+       return "<string><![CDATA[" + this.replace(/\]\]/g, "] ]") + "]]></string>";//.replace(/</g, "&lt;").replace(/&/g, "&amp;")
+}
+
+Number.prototype.toXMLRPC = function(){
+       if(this == parseInt(this)){
+               return "<int>" + this + "</int>";
+       }
+       else if(this == parseFloat(this)){
+               return "<double>" + this + "</double>";
+       }
+       else{
+               return false.toXMLRPC();
+       }
+}
+
+Boolean.prototype.toXMLRPC = function(){
+       if(this) return "<boolean>1</boolean>";
+       else return "<boolean>0</boolean>";
+}
+
+Date.prototype.toXMLRPC = function(){
+       //Could build in possibilities to express dates 
+       //in weeks or other iso8601 possibillities
+       //hmmmm ????
+       //19980717T14:08:55
+       return "<dateTime.iso8601>" + doYear(this.getUTCYear()) + doZero(this.getMonth()) + doZero(this.getUTCDate()) + "T" + doZero(this.getHours()) + ":" + doZero(this.getMinutes()) + ":" + doZero(this.getSeconds()) + "</dateTime.iso8601>";
+       
+       function doZero(nr) {
+               nr = String("0" + nr);
+               return nr.substr(nr.length-2, 2);
+       }
+       
+       function doYear(year) {
+               if(year > 9999 || year < 0) 
+                       XMLRPC.handleError(new Error("Unsupported year: " + year));
+                       
+               year = String("0000" + year)
+               return year.substr(year.length-4, 4);
+       }
+}
+
+Array.prototype.toXMLRPC = function(){
+       var retstr = "<array><data>";
+       for(var i=0;i<this.length;i++){
+               retstr += "<value>" + XMLRPC.getXML(this[i]) + "</value>";
+       }
+       return retstr + "</data></array>";
+}
+
+function VirtualService(servername, oRPC){
+       this.version = '0.91';
+       this.URL = servername;
+       this.multicall = false;
+       this.autoroute = true;
+       this.onerror = null;
+       
+       this.rpc = oRPC;
+       this.receive = {};
+       
+       this.purge = function(receive){
+               return this.rpc.purge(this, receive);
+       }
+       
+       this.revert = function(){
+               this.rpc.revert(this);
+       }
+       
+       this.add = function(name, alias, receive){
+               this.rpc.validateMethodName();if(this.rpc.stop){this.rpc.stop = false;return false}
+               if(receive) this.receive[name] = receive;
+               this[(alias || name)] = new Function('var args = new Array(), i;for(i=0;i<arguments.length;i++){args.push(arguments[i]);};return this.call("' + name + '", args);');
+               return true;
+       }
+       
+       //internal function for sending data
+       this.call = function(name, args){
+               var info = this.rpc.send(this.URL, name, args, this.receive[name], this.multicall, this.autoroute);
+               
+               if(info){
+                       if(!this.multicall) this.autoroute = info[0];
+                       return info[1];
+               }
+               else{
+                       if(this.onerror) this.onerror(XMLRPC.lastError);
+                       return false;
+               }
+       }
+}
+
+
+XMLRPC = {
+       routeServer : "http://www.vcdn.org/cgi-bin/rpcproxy.cgi",
+       autoroute : true,
+       multicall : false,
+
+       services : {},
+       stack : {},
+       queue : new Array(),
+       timers : new Array(),
+       timeout : 30000,
+       
+       ontimeout : null,
+       
+       getService : function(serviceName){
+               //serviceNames cannot contain / or .
+               if(/[\/\.]/.test(serviceName)){
+                       return new VirtualService(serviceName, this);
+               }
+               else if(this.services[serviceName]){
+                       return this.services[serviceName];
+               }
+               else{
+                       try{
+                               var ct = eval(serviceName);
+                               this.services[serviceName] = new ct(this);
+                       }
+                       catch(e){
+                               return false;
+                       }
+               }
+       },
+       
+       purge : function(modConst, receive){
+               if(this.stack[modConst.URL].length){
+                       var info = this.send(modConst.URL, "system.multicall", [this.stack[modConst.URL]], receive, false, modConst.autoroute);
+                       modConst.autoroute = info[0];
+                       this.revert(modConst);
+                       
+                       if(info){
+                               modConst.autoroute = info[0];
+                               return info[1];
+                       }
+                       else{
+                               if(modConst.onerror) modConst.onerror(this.lastError);
+                               return false;
+                       }
+               }
+       },
+       
+       revert : function(modConst){
+               this.stack[modConst.URL] = new Array();
+       },
+       
+       call : function(){
+               //[optional info || receive, servername,] functionname, args......
+               var args = new Array(), i, a = arguments;
+               var servername, methodname, receive, service, info, autoroute, multicall;
+               
+               if(typeof a[0] == "object"){
+                       receive = a[0][0];
+                       servername = a[0][1].URL;
+                       methodname = a[1];
+                       multicall = (a[0][1].supportsMulticall && a[0][1].multicall);
+                       autoroute = a[0][1].autoroute;
+                       service = a[0][1];
+               }
+               else if(typeof a[0] == "function"){
+                       i = 3;
+                       receive = a[0];
+                       servername = a[1];
+                       methodname = a[2];
+               }
+               else{
+                       i = 2;
+                       servername = a[0];
+                       methodname = a[1];
+               }
+                       
+               for(i=i;i<a.length;i++){
+                       args.push(a[i]);
+               }
+               
+               info = this.send(servername, methodname, args, receive, multicall, autoroute);
+               if(info){
+                       (service || this).autoroute = info[0];
+                       return info[1];
+               }
+               else{
+                       if(service && service.onerror) service.onerror(this.lastError);
+                       return false;
+               }
+               
+       },
+       
+       /***
+       * Perform typematching on 'vDunno' and return a boolean value corresponding
+       * to the result of the evaluation-match of the mask-value stated in the 2nd argument.
+       * The 2nd argument is optional (none will be treated as a 0-mask) or a sum of
+       * several masks as follows:
+       * type/s    ->  mask/s
+       * --------------------
+       * undefined ->  0/1 [default]
+       * number    ->  2
+       * boolean   ->  4
+       * string    ->  8
+       * function  -> 16
+       * object    -> 32
+       * --------------------
+       * Examples:
+       * Want [String] only: (eqv. (typeof(vDunno) == 'string') )
+       *  Soya.Common.typematch(unknown, 8)
+       * Anything else than 'undefined' acceptable:
+       *  Soya.Common.typematch(unknown)
+       * Want [Number], [Boolean] or [Function]:
+       *  Soya.Common.typematch(unknown, 2 + 4 + 16)
+       * Want [Number] only:
+       *  Soya.Common.typematch(unknown, 2)
+       **/
+       typematch : function (vDunno, nCase){
+               var nMask;
+               switch(typeof(vDunno)){
+                       case 'number'  : nMask = 2;  break;
+                       case 'boolean' : nMask = 4;  break;
+                       case 'string'  : nMask = 8;  break;
+                       case 'function': nMask = 16; break;
+                       case 'object'  : nMask = 32; break;
+                       default      : nMask = 1;  break;
+               }
+               return Boolean(nMask & (nCase || 62));
+       },
+       
+       getNode : function(data, tree){
+               var nc = 0;//nodeCount
+               //node = 1
+               if(data != null){
+                       for(i=0;i<data.childNodes.length;i++){
+                               if(data.childNodes[i].nodeType == 1){
+                                       if(nc == tree[0]){
+                                               data = data.childNodes[i];
+                                               if(tree.length > 1){
+                                                       tree.shift();
+                                                       data = this.getNode(data, tree);
+                                               }
+                                               return data;
+                                       }
+                                       nc++
+                               }
+                       }
+               }
+               
+               return false;
+       },
+       
+       toObject : function(data){
+               var ret, i;
+               switch(data.tagName){
+                       case "string":
+                               return (data.firstChild) ? new String(data.firstChild.nodeValue) : "";
+                               break;
+                       case "int":
+                       case "i4":
+                       case "double":
+                               return (data.firstChild) ? new Number(data.firstChild.nodeValue) : 0;
+                               break;
+                       case "dateTime.iso8601":
+                               /*
+                               Have to read the spec to be able to completely 
+                               parse all the possibilities in iso8601
+                               07-17-1998 14:08:55
+                               19980717T14:08:55
+                               */
+                               
+                               var sn = (isIE) ? "-" : "/";
+                               
+                               if(/^(\d{4})(\d{2})(\d{2})T(\d{2}):(\d{2}):(\d{2})/.test(data.firstChild.nodeValue)){;//data.text)){
+                       return new Date(RegExp.$2 + sn + RegExp.$3 + sn + 
+                                                               RegExp.$1 + " " + RegExp.$4 + ":" + 
+                                                               RegExp.$5 + ":" + RegExp.$6);
+               }
+                       else{
+                               return new Date();
+                       }
+
+                               break;
+                       case "array":
+                               data = this.getNode(data, [0]);
+                               
+                               if(data && data.tagName == "data"){
+                                       ret = new Array();
+                                       
+                                       var i = 0;
+                                       while(child = this.getNode(data, [i++])){
+                               ret.push(this.toObject(child));
+                                       }
+                                       
+                                       return ret;
+                               }
+                               else{
+                                       this.handleError(new Error("Malformed XMLRPC Message1"));
+                                       return false;
+                               }
+                               break;
+                       case "struct":
+                               ret = {};
+                                       
+                               var i = 0;
+                               while(child = this.getNode(data, [i++])){
+                                       if(child.tagName == "member"){
+                                               ret[this.getNode(child, [0]).firstChild.nodeValue] = this.toObject(this.getNode(child, [1]));
+                                       }
+                                       else{
+                                               this.handleError(new Error("Malformed XMLRPC Message2"));
+                                               return false;
+                                       }
+                               }
+                               
+                               return ret;
+                               break;
+                       case "boolean":
+                               return Boolean(isNaN(parseInt(data.firstChild.nodeValue)) ? (data.firstChild.nodeValue == "true") : parseInt(data.firstChild.nodeValue))
+
+                               break;
+                       case "base64":
+                               return this.decodeBase64(data.firstChild.nodeValue);
+                               break;
+                       case "value":
+                               child = this.getNode(data, [0]);
+                               return (!child) ? ((data.firstChild) ? new String(data.firstChild.nodeValue) : "") : this.toObject(child);
+
+                               break;
+                       default:
+                               this.handleError(new Error("Malformed XMLRPC Message: " + data.tagName));
+                               return false;
+                               break;
+               }
+       },
+       
+       /*** Decode Base64 ******
+       * Original Idea & Code by thomas@saltstorm.net
+       * from Soya.Encode.Base64 [http://soya.saltstorm.net]
+       **/
+       decodeBase64 : function(sEncoded){
+               // Input must be dividable with 4.
+               if(!sEncoded || (sEncoded.length % 4) > 0)
+                 return sEncoded;
+       
+               /* Use NN's built-in base64 decoder if available.
+                  This procedure is horribly slow running under NN4,
+                  so the NN built-in equivalent comes in very handy. :) */
+       
+               else if(typeof(atob) != 'undefined')
+                 return atob(sEncoded);
+       
+               var nBits, i, sDecoded = '';
+               var base64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
+               sEncoded = sEncoded.replace(/\W|=/g, '');
+       
+               for(i=0; i < sEncoded.length; i += 4){
+                       nBits =
+                               (base64.indexOf(sEncoded.charAt(i))   & 0xff) << 18 |
+                               (base64.indexOf(sEncoded.charAt(i+1)) & 0xff) << 12 |
+                               (base64.indexOf(sEncoded.charAt(i+2)) & 0xff) <<  6 |
+                               base64.indexOf(sEncoded.charAt(i+3)) & 0xff;
+                       sDecoded += String.fromCharCode(
+                               (nBits & 0xff0000) >> 16, (nBits & 0xff00) >> 8, nBits & 0xff);
+               }
+       
+               // not sure if the following statement behaves as supposed under
+               // all circumstances, but tests up til now says it does.
+       
+               return sDecoded.substring(0, sDecoded.length -
+                ((sEncoded.charCodeAt(i - 2) == 61) ? 2 :
+                 (sEncoded.charCodeAt(i - 1) == 61 ? 1 : 0)));
+       },
+       
+       getObject : function(type, message){
+               if(type == "HTTP"){
+                       if(isIE)
+                               obj = new ActiveXObject("microsoft.XMLHTTP"); 
+                       else if(isNS)
+                               obj = new XMLHttpRequest();
+               }
+               else if(type == "XMLDOM"){
+                       if(isIE){
+                               obj = new ActiveXObject("microsoft.XMLDOM"); 
+                               obj.loadXML(message)
+                       }else if(isNS){
+                               obj = new DOMParser();
+                               obj = obj.parseFromString(message, "text/xml");
+                       }
+                       
+               }
+               else{
+                       this.handleError(new Error("Unknown Object"));
+               }
+
+               return obj;
+       },
+       
+       validateMethodName : function(name){
+               /*do Checking:
+               
+               The string may only contain identifier characters, 
+               upper and lower-case A-Z, the numeric characters, 0-9, 
+               underscore, dot, colon and slash. 
+               
+               */
+               if(/^[A-Za-z0-9\._\/:]+$/.test(name))
+                       return true
+               else
+                       this.handleError(new Error("Incorrect method name"));
+       },
+       
+       getXML : function(obj){
+               if(typeof obj == "function"){
+                       this.handleError(new Error("Cannot Parse functions"));
+               }else if(obj == null || obj == undefined || (typeof obj == "number" && !isFinite(obj)))
+                       return false.toXMLRPC();
+               else
+                       return obj.toXMLRPC();
+       },
+       
+       handleError : function(e){
+               if(!this.onerror || !this.onerror(e)){
+                       //alert("An error has occured: " + e.message);
+                       throw e;
+               }
+               this.stop = true;
+               this.lastError = e;
+       },
+       
+       cancel : function(id){
+               //You can only cancel a request when it was executed async (I think)
+               if(!this.queue[id]) return false;
+               
+               this.queue[id][0].abort();
+               return true;
+       },
+       
+       send : function(serverAddress, functionName, args, receive, multicall, autoroute){
+               var id, http;
+               //default is sync
+               this.validateMethodName();
+               if(this.stop){this.stop = false; return false;}
+               
+               //setting up multicall
+               multicall = (multicall != null) ? multicall : this.multicall;
+               
+               if(multicall){
+                       if(!this.stack[serverAddress]) this.stack[serverAddress] = new Array();
+                       this.stack[serverAddress].push({methodName : functionName, params : args});
+                       return true;
+               }
+               
+               //creating http object
+               var http = this.getObject("HTTP");
+               
+               //setting some things for async/sync transfers
+               if(!receive || isNS){;
+                       async = false;
+               }
+               else{
+                       async = true;
+                       /* The timer functionality is implemented instead of
+                               the onreadystatechange event because somehow
+                               the calling of this event crashed IE5.x
+                       */
+                       id = this.queue.push([http, receive, null, new Date()])-1;
+                       
+                       this.queue[id][2] = new Function("var id='" + id + "';var dt = new Date(new Date().getTime() - XMLRPC.queue[id][3].getTime());diff = parseInt(dt.getSeconds()*1000 + dt.getMilliseconds());if(diff > XMLRPC.timeout){if(XMLRPC.ontimeout) XMLRPC.ontimeout(); clearInterval(XMLRPC.timers[id]);XMLRPC.cancel(id);return};if(XMLRPC.queue[id][0].readyState == 4){XMLRPC.queue[id][0].onreadystatechange = function(){};XMLRPC.receive(id);clearInterval(XMLRPC.timers[id])}");
+                       this.timers[id] = setInterval("XMLRPC.queue[" + id + "][2]()", 20);
+               }
+               
+               //setting up the routing
+               autoroute = (autoroute || this.autoroute);
+               
+               //'active' is only set when direct sending the message has failed
+               var srv = (autoroute == "active") ? this.routeServer : serverAddress;
+               
+               try{
+                       http.open('POST', srv, async);
+                       http.setRequestHeader("User-Agent", "vcXMLRPC v0.91 (" + navigator.userAgent + ")");
+                       http.setRequestHeader("Host", srv.replace(/^https?:\/{2}([:\[\]\-\w\.]+)\/?.*/, '$1'));
+                       http.setRequestHeader("Content-type", "text/xml");
+                       if(autoroute == "active"){
+                               http.setRequestHeader("X-Proxy-Request", serverAddress);
+                               http.setRequestHeader("X-Compress-Response", "gzip");
+                       }
+               }
+               catch(e){
+                       if(autoroute == true){
+                               //Access has been denied, Routing call.
+                               autoroute = "active";
+                               if(id){
+                                       delete this.queue[id];
+                                       clearInterval(this.timers[id]);
+                               }
+                               return this.send(serverAddress, functionName, args, receive, multicall, autoroute);
+                       }
+                       
+                       //Routing didn't work either..Throwing error
+                       this.handleError(new Error("Could not sent XMLRPC Message (Reason: Access Denied on client)"));
+                       if(this.stop){this.stop = false;return false}
+               }
+               
+               //Construct the message
+               var message = '<?xml version="1.0"?><methodCall><methodName>' + functionName + '</methodName><params>';
+       for(i=0;i<args.length;i++){
+               message += '<param><value>' + this.getXML(args[i]) + '</value></param>';
+               }
+               message += '</params></methodCall>';
+               
+               var xmldom = this.getObject('XMLDOM', message);
+               if(self.DEBUG) alert(message);
+               
+               try{
+                       //send message
+                       http.send(xmldom);
+               }
+               catch(e){
+                       //Most likely the message timed out(what happend to your internet connection?)
+                       this.handleError(new Error("XMLRPC Message not Sent(Reason: " + e.message + ")"));
+                       if(this.stop){this.stop = false;return false}
+               }
+               
+               if(!async && receive)
+                       return [autoroute, receive(this.processResult(http))];
+               else if(receive)
+                       return [autoroute, id];
+               else
+                       return [autoroute, this.processResult(http)];
+       },
+       
+       receive : function(id){
+               //Function for handling async transfers..
+               if(this.queue[id]){
+                       var data = this.processResult(this.queue[id][0]);
+                       this.queue[id][1](data);
+                       delete this.queue[id];
+               }
+               else{
+                       this.handleError(new Error("Error while processing queue"));
+               }
+       },
+       
+       processResult : function(http){
+               if(self.DEBUG) alert(http.responseText);
+               if(http.status == 200){
+                       //getIncoming message
+                  dom = http.responseXML;
+
+                  if(dom){
+                       var rpcErr, main;
+
+                       //Check for XMLRPC Errors
+                       rpcErr = dom.getElementsByTagName("fault");
+                       if(rpcErr.length > 0){
+                               rpcErr = this.toObject(rpcErr[0].firstChild);
+                               this.handleError(new Error(rpcErr.faultCode, rpcErr.faultString));
+                               return false
+                       }
+
+                       //handle method result
+                       main = dom.getElementsByTagName("param");
+                     if(main.length == 0) this.handleError(new Error("Malformed XMLRPC Message"));
+                               data = this.toObject(this.getNode(main[0], [0]));
+
+                               //handle receiving
+                               if(this.onreceive) this.onreceive(data);
+                               return data;
+                  }
+                  else{
+                               this.handleError(new Error("Malformed XMLRPC Message"));
+                       }
+               }
+               else{
+                   e = new Error("HTTP Exception: (" + http.status + ") " + http.statusText + "\n\n" + http.responseText);
+                   e.http_status = http.status;
+                   e.http_statusText = http.statusText;
+                   this.handleError(e);
+                       //this.handleError(new Error("HTTP Exception: (" + http.status + ") " + http.statusText + "\n\n" + http.responseText));
+               }
+       }
+}
+
+//Smell something
+ver = navigator.appVersion;
+app = navigator.appName;
+isNS = Boolean(navigator.productSub)
+//moz_can_do_http = (parseInt(navigator.productSub) >= 20010308)
+
+isIE = (ver.indexOf("MSIE 5") != -1 || ver.indexOf("MSIE 6") != -1) ? 1 : 0;
+isIE55 = (ver.indexOf("MSIE 5.5") != -1) ? 1 : 0;
+
+isOTHER = (!isNS && !isIE) ? 1 : 0;
+
+if(isOTHER) alert("Sorry your browser doesn't support the features of vcXMLRPC");
diff --git a/hermes/scripts/Sandals.wdgt/themes/bluewhite-screen.css b/hermes/scripts/Sandals.wdgt/themes/bluewhite-screen.css
new file mode 100644 (file)
index 0000000..4d3dcdd
--- /dev/null
@@ -0,0 +1,162 @@
+/**
+ * $Horde: hermes/scripts/Sandals.wdgt/themes/bluewhite-screen.css,v 1.1 2008/03/08 06:49:07 bklang Exp $
+ */
+
+body {
+    font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
+    background: #fff;
+}
+a {
+    color: #339;
+}
+.headerbox {
+    border-color: #66a;
+}
+.selected {
+    background: #C6D3FF;
+}
+.header {
+    color: #fff;
+    background: #66a;
+    border-bottom: 1px solid #000;
+}
+.header a {
+    color: #fff;
+}
+.header a:hover, a.header:hover {
+    color: yellow;
+    text-decoration: none;
+}
+a.header {
+    border-bottom: 0;
+    background: transparent;
+}
+a.fixed {
+    color: #339;
+}
+.light {
+    color: #333;
+}
+.smallheader {
+    color: #fff;
+    background: #66a;
+    font-family: Verdana,Helvetica,sans-serif;
+}
+a.smallheader:hover {
+    color: #cce;
+    text-decoration: none;
+}
+.control {
+    background: #ddd;
+}
+.widget {
+    color: #224;
+}
+
+/* Form styles. */
+input, select, textarea {
+    color: #000;
+    background: #f3f3f9;
+    border: 1px solid #669;
+}
+input:focus, textarea:focus {
+    background: #fff;
+    border: 1px solid #99f;
+}
+.button, .button:focus {
+    color: #fff;
+    background: #66a;
+    border: 1px solid #C4C4B8;
+    border-bottom-color: #333;
+    border-right-color: #000;
+    -moz-border-radius: 5px;
+    -webkit-border-radius: 5px;
+}
+.button:hover, .button:focus, a.button:hover, a.button:focus {
+    background: #339;
+}
+
+/* Alternating styles. item0, item1 are deprecated. */
+.rowEven, .item0 {
+    background-color: #eef;
+}
+.rowOdd, .item1 {
+    background-color: #fff;
+}
+
+/* Various popup and status layers. */
+.tooltip, div.nicetitle {
+    color: #fff;
+    background: #66a;
+    border: 2px double #fff;
+}
+
+/* Menu styles. */
+#menu {
+    color: #fff;
+    background: #66a;
+    border-bottom: 2px solid #000;
+}
+#menu a {
+    color: #fff;
+}
+#menu a.current {
+    background: #339;
+    border-bottom: 1px solid #aac;
+    border-right: 1px solid #aac;
+    border-left: 1px solid #000;
+    border-top: 1px solid #000;
+    padding: 2px;
+}
+#menuBottom {
+    background: #eef;
+    border-left: 1px solid #ccc;
+    border-bottom: 1px solid #ccc;
+    -moz-border-radius-bottomleft: 10px;
+    -webkit-border-bottom-left-radius: 10px;
+}
+
+/* Sidebar styles. */
+body.sidebar {
+    background: #fff;
+}
+#sidebarPanel {
+    border-right: 1px solid #006;
+    border-bottom: 1px solid #006;
+    background: #e9e9ff;
+}
+#sidebarPanel span.toggle, #sidebarPanel a {
+    padding: 4px;
+}
+#sidebarPanel a, #sidebarPanel span {
+    color: #006;
+}
+#sidebarPanel a:hover {
+    color: #fff;
+    background: #66a;
+}
+#sidebarPanel a:hover span {
+    color: #fff;
+}
+#expandButton {
+    margin-right: 1px;
+}
+
+/* Tab styles. */
+.tabset {
+    background: none;
+    border-bottom: 1px solid #66a;
+}
+.tabset li a {
+    border: 1px solid #66a;
+    border-bottom-color: #e9e9e9;
+}
+.tabset li.activeTab a {
+    background: #66a;
+    color: #fff;
+    border-bottom: 1px solid #66a;
+}
+.tabset li.activeTab a:hover {
+    background: #66a;
+    color: #fff;
+}
diff --git a/hermes/scripts/Sandals.wdgt/themes/graphics/alerts/alarm.png b/hermes/scripts/Sandals.wdgt/themes/graphics/alerts/alarm.png
new file mode 100644 (file)
index 0000000..70c1e1d
Binary files /dev/null and b/hermes/scripts/Sandals.wdgt/themes/graphics/alerts/alarm.png differ
diff --git a/hermes/scripts/Sandals.wdgt/themes/graphics/alerts/error.png b/hermes/scripts/Sandals.wdgt/themes/graphics/alerts/error.png
new file mode 100644 (file)
index 0000000..d1c6785
Binary files /dev/null and b/hermes/scripts/Sandals.wdgt/themes/graphics/alerts/error.png differ
diff --git a/hermes/scripts/Sandals.wdgt/themes/graphics/alerts/message.png b/hermes/scripts/Sandals.wdgt/themes/graphics/alerts/message.png
new file mode 100644 (file)
index 0000000..1cae1e8
Binary files /dev/null and b/hermes/scripts/Sandals.wdgt/themes/graphics/alerts/message.png differ
diff --git a/hermes/scripts/Sandals.wdgt/themes/graphics/alerts/success.png b/hermes/scripts/Sandals.wdgt/themes/graphics/alerts/success.png
new file mode 100644 (file)
index 0000000..4af9076
Binary files /dev/null and b/hermes/scripts/Sandals.wdgt/themes/graphics/alerts/success.png differ
diff --git a/hermes/scripts/Sandals.wdgt/themes/graphics/alerts/warning.png b/hermes/scripts/Sandals.wdgt/themes/graphics/alerts/warning.png
new file mode 100644 (file)
index 0000000..7f6d50f
Binary files /dev/null and b/hermes/scripts/Sandals.wdgt/themes/graphics/alerts/warning.png differ
diff --git a/hermes/scripts/Sandals.wdgt/themes/graphics/calendar.png b/hermes/scripts/Sandals.wdgt/themes/graphics/calendar.png
new file mode 100644 (file)
index 0000000..2712746
Binary files /dev/null and b/hermes/scripts/Sandals.wdgt/themes/graphics/calendar.png differ
diff --git a/hermes/scripts/Sandals.wdgt/themes/graphics/hermes.png b/hermes/scripts/Sandals.wdgt/themes/graphics/hermes.png
new file mode 100644 (file)
index 0000000..f691913
Binary files /dev/null and b/hermes/scripts/Sandals.wdgt/themes/graphics/hermes.png differ
diff --git a/hermes/scripts/Sandals.wdgt/themes/graphics/required.png b/hermes/scripts/Sandals.wdgt/themes/graphics/required.png
new file mode 100644 (file)
index 0000000..1756362
Binary files /dev/null and b/hermes/scripts/Sandals.wdgt/themes/graphics/required.png differ
diff --git a/hermes/scripts/Sandals.wdgt/themes/graphics/spinner-transparent.gif b/hermes/scripts/Sandals.wdgt/themes/graphics/spinner-transparent.gif
new file mode 100644 (file)
index 0000000..5b33f7e
Binary files /dev/null and b/hermes/scripts/Sandals.wdgt/themes/graphics/spinner-transparent.gif differ
diff --git a/hermes/scripts/Sandals.wdgt/themes/hermes-screen.css b/hermes/scripts/Sandals.wdgt/themes/hermes-screen.css
new file mode 100644 (file)
index 0000000..ffe0348
--- /dev/null
@@ -0,0 +1,40 @@
+/**
+ * $Horde: hermes/scripts/Sandals.wdgt/themes/hermes-screen.css,v 1.1 2008/03/08 06:49:07 bklang Exp $
+ */
+
+/* Special handling for non-printed timesheets. */
+#approval {
+    display: none;
+}
+
+/* Time list tables */
+table.time {
+    width: 100%;
+    margin-bottom: 8px;
+    border-top: 1px solid #ddd;
+    border-left: 1px solid #ddd;
+}
+table.time th {
+    padding: 3px;
+    background: #e9e9e9;
+    border-right: 1px solid #ccc;
+    text-align: left;
+}
+table.time td {
+    padding: 3px;
+    border-right: 1px solid #ddd;
+    border-bottom: 1px solid #ddd;
+}
+table.time th.sortup {
+    background: #bbcbff url("graphics/za.png") center left no-repeat;
+    padding-left: 10px;
+}
+table.time th.sortdown {
+    background: #bbcbff url("graphics/az.png") center left no-repeat;
+    padding-left: 10px;
+}
+table.time tfoot td {
+    background: #ddd;
+    font-weight: bold;
+    padding: 1px;
+}
diff --git a/hermes/scripts/Sandals.wdgt/themes/sandals-screen.css b/hermes/scripts/Sandals.wdgt/themes/sandals-screen.css
new file mode 100644 (file)
index 0000000..fc56db4
--- /dev/null
@@ -0,0 +1,6 @@
+/* $Horde: hermes/scripts/Sandals.wdgt/themes/sandals-screen.css,v 1.2 2008/03/08 06:52:38 bklang Exp $ */
+#title
+{
+       width: 100%;
+       height: 45px;
+}
diff --git a/hermes/scripts/Sandals.wdgt/themes/screen.css b/hermes/scripts/Sandals.wdgt/themes/screen.css
new file mode 100644 (file)
index 0000000..2853887
--- /dev/null
@@ -0,0 +1,795 @@
+/**
+ * $Horde: hermes/scripts/Sandals.wdgt/themes/screen.css,v 1.2 2008/04/08 18:07:29 chuck Exp $
+ */
+
+/* Global default styles. */
+* {
+    /* Zero out all padding and margins to start with for better
+     * cross-browser control. */
+    padding: 0;
+    margin: 0;
+}
+
+body {
+    font-family: Geneva,Arial,Helvetica,sans-serif;
+    font-size: 75%;
+    background: #fff;
+    color: #000;
+}
+
+body.scrollbar-quirk {
+    margin-right: 15px;
+}
+
+p {
+    margin-bottom: 10px;
+}
+
+img, .img, .img:active, .img:hover, .img:visited, .image {
+    border: none;
+    vertical-align: middle;
+    background: transparent;
+}
+/* This must be declared seperately or IE will ignore all of the selectors. */
+input[type=image] {
+    border: none;
+    vertical-align: middle;
+    background: transparent;
+}
+
+ins {
+    background: #cfc;
+}
+del {
+    background: #fcc;
+}
+
+br.spacer {
+    line-height: 8px;
+}
+
+.leftAlign {
+    text-align: left;
+}
+.rightAlign {
+    text-align: right;
+}
+.leftFloat {
+    float: left;
+}
+.rightFloat {
+    float: right;
+}
+
+.box {
+    padding: 3px;
+    border: 1px dashed #999;
+    background: #fff;
+}
+.solidbox {
+    border: 1px solid #000;
+}
+.greybox {
+    border: 1px solid #000;
+    background: #e9e9e9;
+}
+.headerbox {
+    border-left: 1px solid #000;
+    border-right: 1px solid #000;
+    border-bottom: 1px solid #000;
+    background: #fff;
+}
+.roundedBox {
+    -moz-border-radius: 15px;
+    -webkit-border-radius: 15px;
+}
+.roundedTop {
+    -moz-border-radius-topright: 15px;
+    -moz-border-radius-topleft: 15px;
+    -webkit-border-top-right-radius: 15px;
+    -webkit-border-top-left-radius: 15px;
+}
+.roundedBottom {
+    -moz-border-radius-bottomright: 15px;
+    -moz-border-radius-bottomleft: 15px;
+    -webkit-border-bottom-right-radius: 15px;
+    -webkit-border-bottom-left-radius: 15px;
+}
+.header {
+    font-family: Verdana,Helvetica,sans-serif;
+    font-weight: bold;
+    font-size: 125%;
+    padding: 3px;
+}
+.header a:hover, a.header:hover {
+    color: #333;
+}
+.header img {
+    vertical-align: bottom;
+}
+.header input, .header select {
+    font-size: 80%;
+}
+.header .smallheader input, .header .smallheader select {
+    font-size: 100%;
+}
+.header .button, .header .smallheader .button {
+    font-size: 72%;
+}
+.header ul {
+    float: right;
+    text-align: right;
+    font-size: 80%;
+}
+.header li {
+    list-style: none;
+    display: inline;
+}
+
+.nowrap {
+    white-space: nowrap;
+}
+.clear {
+    clear: both;
+    line-height: 0;
+    height: 0;
+}
+
+a {
+    color: blue;
+    text-decoration: none;
+}
+a:hover {
+    text-decoration: underline;
+}
+.selected {
+    background: #bbcbff;
+}
+.selected-over {
+    background: #cef;
+}
+
+/* Table styles. */
+table {
+    border: none;
+}
+td, th {
+    padding: 1px;
+}
+th {
+    color: #333;
+    font-family: Verdana,Helvetica,sans-serif;
+    font-size: 90%;
+}
+td {
+    font-family: Geneva,Arial,Helvetica,sans-serif;
+}
+ul.linedRow {
+    list-style-type: none;
+}
+.linedRow, tr.linedRow td, table.linedRow td, ul.linedRow li {
+    color: #000;
+    background: #fff;
+    border-bottom: 1px solid #ddd;
+}
+table.linedRow, ul.linedRow {
+    border-bottom: none;
+}
+.linedRowSelectedCol, td.linedRowSelectedCol, table.linedRow td.linedRowSelectedCol {
+    color: #000;
+    background: #f3f3f3;
+    border-bottom: 1px solid #ddd;
+}
+.selectedRow, tr.selectedRow td {
+    background: #ffc;
+}
+.selectedRow:hover, tr.selectedRow:hover td, .selectedRow-over, tr.selectedRow-over td {
+    background: #ffd;
+}
+table.nopadding td, table.nopadding th {
+    padding: 0;
+}
+.sortup {
+    background: #bbcbff url("graphics/za.png") center left no-repeat;
+    padding-left: 10px;
+}
+.sortdown {
+    background: #bbcbff url("graphics/az.png") center left no-repeat;
+    padding-left: 10px;
+}
+/* Alternating styles. item0, item1 are deprecated. */
+.striped, .rowEven, .item0 {
+    background-color: #eee;
+}
+.rowOdd, .item1 {
+    background-color: #ddd;
+}
+
+.widget {
+    font-family: Verdana,Helvetica,sans-serif;
+    font-size: 90%;
+}
+
+.light {
+    font-family: Geneva,Arial,Helvetica,sans-serif;
+}
+.smallheader {
+    font-family: Geneva,Arial,Helvetica,sans-serif;
+    font-size: 100%;
+    font-weight: bold;
+}
+.header .smallheader {
+    font-size: 80%;
+}
+.header .smallheader a.smallheader {
+    font-size: 100%;
+}
+small, .small {
+    font-size: 80%;
+}
+.control {
+    color: #000;
+    background: #ccc;
+    border-bottom: 1px solid #999;
+    padding: 1px;
+}
+.item {
+    color: #000;
+    background: #eee;
+}
+.accessKey {
+    text-decoration: underline;
+}
+.text {
+    color: #000;
+    background: #fff;
+}
+
+/* Form styles. */
+.form table {
+    width: 100%;
+    padding: 2px;
+    border-collapse: collapse;
+}
+form[action^="https://"] input[type="submit"] {
+    background-image: url("graphics/locked.png");
+    background-position: 95% center;
+    background-repeat: no-repeat;
+    padding-right: 10%;
+}
+.htmlarea .statusBar .statusBarTree a {
+    font: inherit;
+}
+.htmlarea table {
+    width: auto;
+}
+.form-error {
+    color: #f00;
+}
+input, select, textarea {
+    font-family: Geneva,Arial,Helvetica,sans-serif;
+    font-size: 100%;
+    font-weight: normal;
+}
+input {
+    padding: 1px;
+}
+option {
+    padding: 0 5px 0 3px;
+}
+.checkbox {
+    border: 0;
+    height: 14px;
+    width: 14px;
+    background: transparent;
+}
+.button {
+    cursor: pointer;
+    font-size: 90%;
+    font-family: Verdana,Helvetica,sans-serif;
+    padding: 1px 6px;
+}
+a.button {
+    padding: 2px 8px;
+    font-weight: normal;
+    text-decoration: none;
+}
+
+pre, code, .fixed {
+    font-size: 100%;
+    font-family: "Lucida Console",Courier,"Courier New";
+}
+
+/* Styles for email-like messages. */
+.signature {
+    color: #ccc;
+}
+.signature-fixed {
+    color: #ccc;
+    font-family: "Lucida Console",Courier,"Courier New";
+}
+.citation {
+    margin: 1em 0;
+    padding-left: 1em;
+    border-left-width: 1px;
+    border-left-style: solid;
+}
+.quoted1 {
+    color: #606;
+    border-left-color: #606;
+}
+.quoted2 {
+    color: #077;
+    border-left-color: #077;
+}
+.quoted3 {
+    color: #900;
+    border-left-color: #900;
+}
+.quoted4 {
+    color: #009;
+    border-left-color: #009;
+}
+.quoted5 {
+    color: #b60;
+    border-left-color: #b60;
+}
+
+/* Various popup and status layers. */
+.notices {
+    margin: .5em 8px;
+    list-style-type: none;
+}
+.notices li {
+    font-weight: bold;
+    color: #000;
+    background: #ffc;
+    border: 1px solid #aaa;
+    padding: 1px 1px 1px 5px;
+}
+.notices img {
+    margin-right: .5em;
+}
+.tooltip, div.nicetitle {
+    font: 11px/12px Verdana,Arial,serif;
+    color: #000;
+    background: #ffc;
+    border: 1px solid #000;
+    padding: 5px;
+    z-index: 1001;
+    -moz-border-radius: 3px;
+    -webkit-border-radius: 3px;
+}
+div.nicetitle {
+    position: absolute;
+    overflow: hidden;
+    opacity: .90;
+}
+div.nicetitle pre {
+    font-family: "Lucida Console",Courier,"Courier New";
+}
+.inProgress {
+    font: 10px Geneva,Arial,Helvetica,sans-serif;
+    padding: 2px;
+    color: #fff;
+    background: #f00;
+}
+
+/* Tree styles. */
+.treeRow {
+    overflow: hidden;
+    min-height: 20px;
+    clear: both;
+}
+
+/* Menu styles. */
+#menu {
+    overflow: hidden;
+    min-height: 50px;
+    font-family: Verdana,Helvetica,sans-serif;
+    margin-bottom: 8px;
+}
+#menu ul {
+    padding: 5px;
+}
+#menu li {
+    list-style-type: none;
+    text-align: center;
+    margin: 2px;
+    float: left;
+}
+#menu li.separator {
+    width: 40px;
+}
+#menu a {
+    display: block;
+    white-space: pre;
+    font-family: Verdana,Helvetica,sans-serif;
+    font-size: 90%;
+    text-decoration: none;
+    padding: 3px;
+    -moz-border-radius: 3px;
+    -webkit-border-radius: 3px;
+}
+#menu h1 {
+    font-family: Verdana,Helvetica,sans-serif;
+    font-weight: bold;
+    font-size: 150%;
+    line-height: 48px;
+    vertical-align: bottom;
+}
+#menu input, #menu select {
+    margin-top: 16px;
+}
+#menuBottom {
+    margin: -8px 0 0 0;
+    padding: 4px;
+    float: right;
+    text-align: right;
+    font-size: 90%;
+    background: #eee;
+}
+
+/* Sidebar styles. */
+.sidebar #menu {
+    margin: 0;
+}
+#sidebarPanel {
+    -moz-border-radius-bottomright: 15px;
+    -webkit-border-bottom-right-radius: 15px;
+    padding-top: 5px;
+    padding-bottom: 10px;
+    background: #eee;
+    white-space: nowrap;
+}
+#sidebarPanel a, #sidebarPanel span {
+    color: #000;
+    white-space: pre;
+    font-family: Verdana,Helvetica,sans-serif;
+    font-size: 90%;
+    text-decoration: none;
+}
+#sidebarPanel span.accessKey {
+    text-decoration: underline;
+}
+#sidebarPanel span.toggle {
+    cursor: pointer;
+}
+#sidebarPanel a:hover {
+    color: #000;
+}
+
+/* Pager. */
+.pager {
+    text-align: center;
+}
+
+/* Tab styles. */
+.tabset {
+    float: left;
+    width: 100%;
+    font-weight: bold;
+    background: url("graphics/tab_bottom.gif") repeat-x bottom;
+}
+.tabset ul {
+    margin-top: 4px;
+    padding-left: 8px;
+    list-style: none;
+}
+.tabset li {
+    float: left;
+    margin: -3px 2px 0 0;
+    white-space: nowrap;
+}
+.tabset li a {
+    color: #000;
+    background-color: #e9e9e9;
+    padding: 2px 0.5em;
+    display: block;
+    border: 1px solid #000;
+    border-bottom-color: #e9e9e9;
+    -moz-border-radius-topleft: 10px;
+    -moz-border-radius-topright: 10px;
+    -moz-border-top-left-radius: 10px;
+    -moz-border-top-right-radius: 10px;
+}
+.tabset li a:hover {
+    text-decoration: none;
+    background-color: #fff;
+}
+.tabset li.activeTab a {
+    border-bottom: 1px solid #000;
+    background-color: #000;
+    color: #fff;
+}
+.tabset li.activeTab a:hover {
+}
+.tabset img {
+    display: block;
+    float: left;
+    padding-right: 2px;
+}
+
+/* Preferences. */
+.prefsOverview div {
+    float: left;
+}
+.prefsOverview div div {
+    padding-left: 10px;
+    width: 95%;
+}
+.prefsOverview h2 {
+    font-size: 150%;
+    font-weight: bold;
+}
+.prefsOverview dt a {
+    display: block;
+    padding: 2px;
+    font-weight: bold;
+    border: 1px solid #000;
+    border-bottom: 1px solid #446;
+    background: #e9e9e9;
+}
+.prefsOverview dd {
+    padding: 2px;
+    margin-bottom: 10px;
+    color: #000;
+    background: #fff;
+    border-left: 1px solid #000;
+    border-right: 1px solid #000;
+    border-bottom: 1px solid #000;
+}
+div.prefsContainer {
+    padding: 1em;
+}
+div.prefsContainer p {
+    padding-top: 1em;
+}
+
+/* Block styles. */
+.currentBlock {
+    border: 2px solid red;
+}
+#googlesearch {
+    padding: .3em;
+}
+
+/* Help styles. */
+body.help_about, body.help_entry {
+    margin: 0.5em;
+}
+.helplink {
+    cursor: help;
+}
+.help {
+    background: #fff;
+    color: #000;
+}
+.help h1 {
+    font-size: 125%;
+    padding-top: 0.5em;
+    padding-bottom: 0.5em;
+}
+.help h2 {
+    padding-top: 1em;
+    padding-bottom: 0.5em;
+    font-size: 110%;
+}
+.help p {
+    margin-left: 1em;
+    margin-right: 1em;
+    margin-bottom: 1em;
+}
+.help em {
+    display: block;
+    padding: 5px;
+    margin-left: 1em;
+    margin-right: 1em;
+    margin-bottom: 1em;
+}
+.help em.helpTip {
+    color: #090;
+    background: #e0f0e0;
+}
+.help em.helpWarn {
+    color: #900;
+    background: #f0e0e0;
+}
+
+/* Source markup styles. */
+table.lineNumbered * {
+    font-family: "Lucida Console",Courier,"Courier New";
+    font-size: 100%;
+    line-height: 16px;
+}
+.lineNumbered th {
+    background: #e9e9e9;
+    border-right: 1px solid #e0e0e0;
+    border-bottom: none;
+    padding-left: 10px;
+    padding-right: 10px;
+    text-align: right;
+}
+.lineNumbered td {
+    vertical-align: top;
+    width: 100%;
+    white-space: pre;
+    background: #fff;
+    padding-left: 10px;
+}
+.parentheses {
+    color: #2a6;
+    font-weight: bold;
+}
+.comment {
+    color: #aac;
+}
+.htag {
+    color: #569;
+    background: #d5d6da;
+    font-weight: bold;
+}
+.metac {
+    color: #0ff;
+    background: #d5d6da;
+}
+.id {
+    color: #e82;
+    background: #e4e4e0;
+}
+.attr {
+    color: #6af;
+}
+.value {
+    color: #d46;
+}
+.color {
+    color: #f57;
+}
+.eol {
+    color: #26e;
+}
+.url {
+    color: #962;
+}
+.file {
+    color: #444;
+    background: #fe4;
+}
+.class {
+    font-style: italic;
+}
+
+/* Drop shadows. */
+.dropShadow {
+    float: left;
+    background: url("graphics/shadow.png") no-repeat bottom right !important;
+    background: url("graphics/shadow.gif") no-repeat bottom right;
+    margin: 10px 0 0 5px;
+}
+
+.dropShadow img {
+    display: block;
+    position: relative;
+    background: #fff;
+    border: 1px solid #666;
+    margin: -3px 5px 5px -3px;
+    padding: 2px;
+}
+
+/* MIME styles. */
+.mimeStatusMessage, .mimeStatusWarning, .mimeHeaders, .mimePartInfo {
+    padding: 1px;
+    margin-bottom: 2px;
+    font-family: Verdana,Arial,serif;
+    font-size: 90%;
+}
+.mimeStatusMessage {
+    color: #000;
+    background: #ffc;
+    border: 1px solid #fff760;
+    width: 100%;
+}
+.mimeStatusWarning {
+    border: 1px solid #800;
+    background: #e81222;
+    color: #fff;
+    width: 100%;
+}
+.mimeHeaders {
+    border: 1px solid #ccc;
+    background: #f9f9f9;
+    width: 100%;
+    font-family: "Lucida Console",Courier,"Courier New";
+}
+.mimePartInfo {
+    border: 1px solid #ccc;
+    background: #f9f9f9;
+}
+.mimePartInfo, .mimePartInfo a {
+    color: #666;
+}
+.mimeStatusIcon {
+    vertical-align: middle;
+    width: 1%;
+}
+.download {
+    padding: 2px 20px 2px 0;
+    background: transparent url("graphics/download.png") center right no-repeat;
+}
+
+/* Calendar Popup. */
+table.calendarPopup {
+    border: 1px solid #ccc;
+    background: #fff;
+    width: auto;
+    z-index: 1001;
+}
+.calendarPopup thead {
+    font-weight: bold;
+}
+.calendarPopup th {
+    border-bottom: 1px solid #ccc;
+}
+.calendarPopup tbody {
+    text-align: right;
+}
+.calendarPopup td {
+    font-family: tahoma;
+    font-size: 75%;
+}
+.calendarPopup a {
+    color: black;
+    display: block;
+    padding: 2px;
+}
+.calendarPopup tbody a:hover {
+    background: #ffc;
+}
+.calendarPopup .current {
+    font-weight: bold;
+    background: #eef;
+}
+.calendarPopup .today {
+    font-weight: bold;
+}
+
+.hidden {
+    display: none;
+}
+
+/* Redbox styles. */
+#RB_overlay {
+    position: absolute;
+    z-index: 100;
+    width: 100%;
+    height: 100%;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    min-height: 100%;
+    background-color: #000;
+    opacity: .6;
+    filter: alpha(opacity=60);
+}
+#RB_loading {
+    z-index: 101;
+    width: 70px;
+    margin-left: auto;
+    margin-right: auto;
+    margin-top: 200px;
+    padding-bottom: 30px;
+    text-align: center;
+    background: url("graphics/redbox_spinner.gif") no-repeat bottom center;
+}
+#RB_window {
+    z-index: 102;
+    background-color: #fff;
+    display: block;
+    text-align: left;
+    overflow: hidden;
+    margin: 20px auto 0 auto;
+    position: fixed;
+    position: absolute;
+}
diff --git a/hermes/scripts/purge.php b/hermes/scripts/purge.php
new file mode 100755 (executable)
index 0000000..b9011db
--- /dev/null
@@ -0,0 +1,27 @@
+#!/usr/bin/php
+<?php
+/**
+ * $Horde: hermes/scripts/purge.php,v 1.14 2009/07/09 06:08:43 slusarz Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ *
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ */
+
+@define('HERMES_BASE', dirname(__FILE__) . '/..');
+@define('HORDE_BASE', dirname(__FILE__) . '/../..');
+require_once HORDE_BASE . '/lib/core.php';
+
+// Registry
+$registry = Horde_Registry::singleton();
+$registry->pushApp('hermes', false);
+
+// Hermes base libraries.
+require_once HERMES_BASE . '/lib/Hermes.php';
+$hermes = &Hermes::getDriver();
+
+printf(_("Deleting data that was exported/billed more than %s days ago.\n"), $conf['time']['days_to_keep']);
+$hermes->purge();
diff --git a/hermes/scripts/sql/hermes.mssql.sql b/hermes/scripts/sql/hermes.mssql.sql
new file mode 100644 (file)
index 0000000..16dd6f4
--- /dev/null
@@ -0,0 +1,51 @@
+-- $Horde: hermes/scripts/sql/hermes.mssql.sql,v 1.7 2008/06/30 09:03:11 jan Exp $
+
+CREATE TABLE hermes_timeslices (
+    timeslice_id           INT NOT NULL,
+    clientjob_id           VARCHAR(255) NOT NULL,
+    employee_id            VARCHAR(255) NOT NULL,
+    jobtype_id             INT NOT NULL,
+    timeslice_hours        NUMERIC(10, 2) NOT NULL,
+    timeslice_rate         NUMERIC(10, 2),
+    timeslice_isbillable   SMALLINT DEFAULT 0 NOT NULL,
+    timeslice_date         INT NOT NULL,
+    timeslice_description  VARCHAR(MAX) NOT NULL,
+    timeslice_note         VARCHAR(MAX),
+    timeslice_submitted    SMALLINT DEFAULT 0 NOT NULL,
+    timeslice_exported     SMALLINT DEFAULT 0 NOT NULL,
+    costobject_id          VARCHAR(255),
+--
+    PRIMARY KEY (timeslice_id)
+);
+
+CREATE TABLE hermes_jobtypes (
+    jobtype_id          INT NOT NULL,
+    jobtype_name        VARCHAR(255),
+    jobtype_enabled     SMALLINT DEFAULT 1 NOT NULL,
+    jobtype_rate        NUMERIC(10, 2),
+    jobtype_billable    SMALLINT DEFAULT 0 NOT NULL,
+--
+    PRIMARY KEY (jobtype_id)
+);
+
+CREATE TABLE hermes_clientjobs (
+    clientjob_id                VARCHAR(255) NOT NULL,
+    clientjob_enterdescription  SMALLINT DEFAULT 1 NOT NULL,
+    clientjob_exportid          VARCHAR(255),
+--
+    PRIMARY KEY (clientjob_id)
+);
+
+CREATE TABLE hermes_deliverables (
+    deliverable_id          INT NOT NULL,
+    client_id               VARCHAR(250) NOT NULL,
+    deliverable_name        VARCHAR(250) NOT NULL,
+    deliverable_parent      INT,
+    deliverable_estimate    NUMERIC(10, 2),
+    deliverable_active      SMALLINT DEFAULT 1 NOT NULL,
+    deliverable_description VARCHAR(MAX),
+--
+    PRIMARY KEY (deliverable_id)
+);
+
+CREATE INDEX hermes_deliverables_client ON hermes_deliverables (client_id, deliverable_name);
diff --git a/hermes/scripts/sql/hermes.oci8.sql b/hermes/scripts/sql/hermes.oci8.sql
new file mode 100644 (file)
index 0000000..c72abfd
--- /dev/null
@@ -0,0 +1,51 @@
+-- $Horde: hermes/scripts/sql/hermes.oci8.sql,v 1.8 2009/10/19 11:18:26 jan Exp $
+
+CREATE TABLE hermes_timeslices (
+    timeslice_id           NUMBER(16) NOT NULL,
+    clientjob_id           VARCHAR2(255) NOT NULL,
+    employee_id            VARCHAR2(255) NOT NULL,
+    jobtype_id             NUMBER(16) NOT NULL,
+    timeslice_hours        NUMBER(10, 2) NOT NULL,
+    timeslice_rate         NUMBER(10, 2),
+    timeslice_isbillable   NUMBER(1) DEFAULT 0 NOT NULL,
+    timeslice_date         NUMBER(16) NOT NULL,
+    timeslice_description  CLOB NOT NULL,
+    timeslice_note         CLOB,
+    timeslice_submitted    NUMBER(1) DEFAULT 0 NOT NULL,
+    timeslice_exported     NUMBER(1) DEFAULT 0 NOT NULL,
+    costobject_id          VARCHAR2(255),
+--
+    PRIMARY KEY (timeslice_id)
+);
+
+CREATE TABLE hermes_jobtypes (
+    jobtype_id          NUMBER(16) NOT NULL,
+    jobtype_name        VARCHAR2(255),
+    jobtype_enabled     NUMBER(1) DEFAULT 1 NOT NULL,
+    jobtype_rate        NUMBER(10, 2),
+    jobtype_billable    NUMBER(1) DEFAULT 0 NOT NULL,
+--
+    PRIMARY KEY (jobtype_id)
+);
+
+CREATE TABLE hermes_clientjobs (
+    clientjob_id                VARCHAR2(255) NOT NULL,
+    clientjob_enterdescription  NUMBER(1) DEFAULT 1 NOT NULL,
+    clientjob_exportid          VARCHAR2(255),
+--
+    PRIMARY KEY (clientjob_id)
+);
+
+CREATE TABLE hermes_deliverables (
+    deliverable_id          NUMBER(16) NOT NULL,
+    client_id               VARCHAR2(250) NOT NULL,
+    deliverable_name        VARCHAR2(250) NOT NULL,
+    deliverable_parent      NUMBER(16),
+    deliverable_estimate    NUMBER(10, 2),
+    deliverable_active      NUMBER(1) DEFAULT 1 NOT NULL,
+    deliverable_description CLOB,
+--
+    PRIMARY KEY (deliverable_id)
+);
+
+CREATE INDEX hermes_deliverables_client ON hermes_deliverables (client_id, deliverable_name);
diff --git a/hermes/scripts/sql/hermes.sql b/hermes/scripts/sql/hermes.sql
new file mode 100644 (file)
index 0000000..1562baa
--- /dev/null
@@ -0,0 +1,52 @@
+-- $Horde: hermes/scripts/sql/hermes.sql,v 1.15 2008/06/30 22:39:27 jan Exp $
+
+CREATE TABLE hermes_timeslices (
+    timeslice_id           INT NOT NULL,
+    clientjob_id           VARCHAR(255) NOT NULL,
+    employee_id            VARCHAR(255) NOT NULL,
+    jobtype_id             INT NOT NULL,
+    timeslice_hours        NUMERIC(10, 2) NOT NULL,
+    timeslice_rate         NUMERIC(10, 2),
+    timeslice_isbillable   SMALLINT DEFAULT 0 NOT NULL,
+    timeslice_date         INT NOT NULL,
+    timeslice_description  TEXT NOT NULL,
+    timeslice_note         TEXT,
+    timeslice_submitted    SMALLINT DEFAULT 0 NOT NULL,
+    timeslice_exported     SMALLINT DEFAULT 0 NOT NULL,
+    costobject_id          VARCHAR(255),
+--
+    PRIMARY KEY (timeslice_id)
+);
+
+CREATE TABLE hermes_jobtypes (
+    jobtype_id          INT NOT NULL,
+    jobtype_name        VARCHAR(255),
+    jobtype_enabled     SMALLINT DEFAULT 1 NOT NULL,
+    jobtype_rate        NUMERIC(10, 2),
+    jobtype_billable    SMALLINT DEFAULT 0 NOT NULL,
+--
+    PRIMARY KEY (jobtype_id)
+);
+
+CREATE TABLE hermes_clientjobs (
+    clientjob_id                VARCHAR(255) NOT NULL,
+    clientjob_enterdescription  SMALLINT DEFAULT 1 NOT NULL,
+    clientjob_exportid          VARCHAR(255),
+--
+    PRIMARY KEY (clientjob_id)
+);
+
+CREATE TABLE hermes_deliverables (
+    deliverable_id          INT NOT NULL,
+    client_id               VARCHAR(255) NOT NULL,
+    deliverable_name        VARCHAR(255) NOT NULL,
+    deliverable_parent      INT,
+    deliverable_estimate    NUMERIC(10, 2),
+    deliverable_active      SMALLINT DEFAULT 1 NOT NULL,
+    deliverable_description TEXT,
+--
+    PRIMARY KEY (deliverable_id)
+);
+
+CREATE INDEX hermes_deliverables_client ON hermes_deliverables (client_id);
+CREATE INDEX hermes_deliverables_active ON hermes_deliverables (deliverable_active);
diff --git a/hermes/scripts/sql/hermes.xml b/hermes/scripts/sql/hermes.xml
new file mode 100644 (file)
index 0000000..2b5a072
--- /dev/null
@@ -0,0 +1,315 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<database>
+
+ <name><variable>name</variable></name>
+ <create>false</create>
+ <overwrite>false</overwrite>
+
+ <table>
+
+  <name>hermes_clientjobs</name>
+
+  <declaration>
+
+   <field>
+    <name>clientjob_id</name>
+    <type>text</type>
+    <default></default>
+    <notnull>true</notnull>
+    <length>255</length>
+   </field>
+
+   <field>
+    <name>clientjob_enterdescription</name>
+    <type>integer</type>
+    <default>1</default>
+    <notnull>true</notnull>
+    <length>1</length>
+   </field>
+
+   <field>
+    <name>clientjob_exportid</name>
+    <type>text</type>
+    <default></default>
+    <notnull>false</notnull>
+    <length>255</length>
+   </field>
+
+   <index>
+    <name>hermes_clientjobs_pKey</name>
+    <primary>true</primary>
+    <field>
+     <name>clientjob_id</name>
+     <sorting>ascending</sorting>
+    </field>
+   </index>
+
+  </declaration>
+
+ </table>
+
+ <table>
+
+  <name>hermes_deliverables</name>
+
+  <declaration>
+
+   <field>
+    <name>deliverable_id</name>
+    <type>integer</type>
+    <default>0</default>
+    <notnull>true</notnull>
+   </field>
+
+   <field>
+    <name>client_id</name>
+    <type>text</type>
+    <default></default>
+    <notnull>true</notnull>
+    <length>255</length>
+   </field>
+
+   <field>
+    <name>deliverable_name</name>
+    <type>text</type>
+    <default></default>
+    <notnull>true</notnull>
+    <length>255</length>
+   </field>
+
+   <field>
+    <name>deliverable_parent</name>
+    <type>integer</type>
+    <default></default>
+    <notnull>false</notnull>
+   </field>
+
+   <field>
+    <name>deliverable_estimate</name>
+    <type>decimal</type>
+    <default></default>
+    <notnull>false</notnull>
+    <length>10,2</length>
+   </field>
+
+   <field>
+    <name>deliverable_active</name>
+    <type>integer</type>
+    <default>1</default>
+    <notnull>true</notnull>
+    <length>1</length>
+   </field>
+
+   <field>
+    <name>deliverable_description</name>
+    <type>text</type>
+    <default></default>
+    <notnull>false</notnull>
+   </field>
+
+   <index>
+    <name>hermes_deliverables_client</name>
+    <field>
+     <name>client_id</name>
+     <sorting>ascending</sorting>
+    </field>
+   </index>
+
+   <index>
+    <name>hermes_deliverables_active</name>
+    <field>
+     <name>deliverable_active</name>
+     <sorting>ascending</sorting>
+    </field>
+   </index>
+
+   <index>
+    <name>hermes_deliverables_pKey</name>
+    <primary>true</primary>
+    <field>
+     <name>deliverable_id</name>
+     <sorting>ascending</sorting>
+    </field>
+   </index>
+
+  </declaration>
+
+ </table>
+
+ <table>
+
+  <name>hermes_jobtypes</name>
+
+  <declaration>
+
+   <field>
+    <name>jobtype_id</name>
+    <type>integer</type>
+    <default>0</default>
+    <notnull>true</notnull>
+   </field>
+
+   <field>
+    <name>jobtype_name</name>
+    <type>text</type>
+    <default></default>
+    <notnull>false</notnull>
+    <length>255</length>
+   </field>
+
+   <field>
+    <name>jobtype_enabled</name>
+    <type>integer</type>
+    <default>1</default>
+    <notnull>true</notnull>
+    <length>1</length>
+   </field>
+
+   <field>
+    <name>jobtype_rate</name>
+    <type>decimal</type>
+    <default></default>
+    <notnull>false</notnull>
+    <length>10,2</length>
+   </field>
+
+   <field>
+    <name>jobtype_billable</name>
+    <type>integer</type>
+    <default>0</default>
+    <notnull>true</notnull>
+    <length>1</length>
+   </field>
+
+   <index>
+    <name>hermes_jobtypes_pKey</name>
+    <primary>true</primary>
+    <field>
+     <name>jobtype_id</name>
+     <sorting>ascending</sorting>
+    </field>
+   </index>
+
+  </declaration>
+
+ </table>
+
+ <table>
+
+  <name>hermes_timeslices</name>
+
+  <declaration>
+
+   <field>
+    <name>timeslice_id</name>
+    <type>integer</type>
+    <default>0</default>
+    <notnull>true</notnull>
+   </field>
+
+   <field>
+    <name>clientjob_id</name>
+    <type>text</type>
+    <default></default>
+    <notnull>true</notnull>
+    <length>255</length>
+   </field>
+
+   <field>
+    <name>employee_id</name>
+    <type>text</type>
+    <default></default>
+    <notnull>true</notnull>
+    <length>255</length>
+   </field>
+
+   <field>
+    <name>jobtype_id</name>
+    <type>integer</type>
+    <default>0</default>
+    <notnull>true</notnull>
+   </field>
+
+   <field>
+    <name>timeslice_hours</name>
+    <type>decimal</type>
+    <default>0.00</default>
+    <notnull>true</notnull>
+    <length>10,2</length>
+   </field>
+
+   <field>
+    <name>timeslice_rate</name>
+    <type>decimal</type>
+    <default></default>
+    <notnull>false</notnull>
+    <length>10,2</length>
+   </field>
+
+   <field>
+    <name>timeslice_isbillable</name>
+    <type>integer</type>
+    <default>0</default>
+    <notnull>true</notnull>
+    <length>1</length>
+   </field>
+
+   <field>
+    <name>timeslice_date</name>
+    <type>integer</type>
+    <default>0</default>
+    <notnull>true</notnull>
+   </field>
+
+   <field>
+    <name>timeslice_description</name>
+    <type>text</type>
+    <default></default>
+    <notnull>true</notnull>
+   </field>
+
+   <field>
+    <name>timeslice_note</name>
+    <type>text</type>
+    <default></default>
+    <notnull>false</notnull>
+   </field>
+
+   <field>
+    <name>timeslice_submitted</name>
+    <type>integer</type>
+    <default>0</default>
+    <notnull>true</notnull>
+    <length>1</length>
+   </field>
+
+   <field>
+    <name>timeslice_exported</name>
+    <type>integer</type>
+    <default>0</default>
+    <notnull>true</notnull>
+    <length>1</length>
+   </field>
+
+   <field>
+    <name>costobject_id</name>
+    <type>text</type>
+    <default></default>
+    <notnull>false</notnull>
+    <length>255</length>
+   </field>
+
+   <index>
+    <name>hermes_timeslices_pKey</name>
+    <primary>true</primary>
+    <field>
+     <name>timeslice_id</name>
+     <sorting>ascending</sorting>
+    </field>
+   </index>
+
+  </declaration>
+
+ </table>
+
+</database>
diff --git a/hermes/scripts/upgrades/2007-04-20_drop_invoicing.sql b/hermes/scripts/upgrades/2007-04-20_drop_invoicing.sql
new file mode 100644 (file)
index 0000000..d478478
--- /dev/null
@@ -0,0 +1,5 @@
+-- $Horde: hermes/scripts/upgrades/2007-04-20_drop_invoicing.sql,v 1.1 2007/04/19 23:14:43 jan Exp $
+
+DROP TABLE hermes_invoice_batches;
+DROP TABLE hermes_invoices;
+DROP TABLE hermes_invoice_items;
diff --git a/hermes/scripts/upgrades/2007-04-25_add_jobtype_rate.sql b/hermes/scripts/upgrades/2007-04-25_add_jobtype_rate.sql
new file mode 100644 (file)
index 0000000..3c110c9
--- /dev/null
@@ -0,0 +1 @@
+ALTER TABLE hermes_jobtypes ADD jobtype_rate FLOAT;
diff --git a/hermes/scripts/upgrades/2007-10-17_add_jobtype_billable.sql b/hermes/scripts/upgrades/2007-10-17_add_jobtype_billable.sql
new file mode 100644 (file)
index 0000000..2ff9cc6
--- /dev/null
@@ -0,0 +1 @@
+ALTER TABLE hermes_jobtypes ADD jobtype_billable SMALLINT DEFAULT 1 NOT NULL;
diff --git a/hermes/scripts/upgrades/2008-07-01_deliverables_index.sql b/hermes/scripts/upgrades/2008-07-01_deliverables_index.sql
new file mode 100644 (file)
index 0000000..6390de3
--- /dev/null
@@ -0,0 +1,3 @@
+ALTER TABLE hermes_deliverables DROP INDEX hermes_deliverables_client;
+CREATE INDEX hermes_deliverables_client ON hermes_deliverables (client_id);
+CREATE INDEX hermes_deliverables_active ON hermes_deliverables (deliverable_active);
diff --git a/hermes/search.php b/hermes/search.php
new file mode 100644 (file)
index 0000000..2e4643c
--- /dev/null
@@ -0,0 +1,141 @@
+<?php
+/**
+ * $Horde: hermes/search.php,v 1.47 2009/12/10 17:42:31 jan Exp $
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ *
+ * @author Jason M. Felice <jason.m.felice@gmail.com>
+ */
+
+@define('HERMES_BASE', dirname(__FILE__));
+require_once HERMES_BASE . '/lib/base.php';
+require_once HERMES_BASE . '/lib/Forms/Export.php';
+require_once HERMES_BASE . '/lib/Forms/Search.php';
+require_once HERMES_BASE . '/lib/Forms/Time.php';
+require_once HERMES_BASE . '/lib/Table.php';
+require_once 'Horde/Identity.php';
+require_once 'Horde/Data.php';
+
+$vars = Horde_Variables::getDefaultVariables();
+
+$delete = $vars->get('delete');
+if (!empty($delete)) {
+    $result = $hermes->updateTime(array(array('id' => $delete, 'delete' => true)));
+    if (is_a($result, 'PEAR_Error')) {
+        Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+        $notification->push(sprintf(_("There was an error deleting the time: %s"), $result->getMessage()), 'horde.error');
+    } else {
+        $notification->push(_("The time entry was successfully deleted."), 'horde.success');
+        $vars->remove('delete');
+    }
+}
+
+$criteria = null;
+
+$formname = $vars->get('formname');
+switch ($formname) {
+case 'searchform':
+    $form = new SearchForm($vars);
+    $form->validate($vars);
+    $criteria = $form->getSearchCriteria($vars);
+    if (is_null($criteria)) {
+        unset($_SESSION['hermes_search_criteria']);
+    } else {
+        $_SESSION['hermes_search_criteria'] = serialize($vars);
+    }
+    break;
+
+case 'exportform':
+    if (!isset($_SESSION['hermes_search_criteria'])) {
+        $notification->push(_("No search to export!"), 'horde.error');
+    } else {
+        $searchVars = unserialize($_SESSION['hermes_search_criteria']);
+        $searchForm = new SearchForm($searchVars);
+        $criteria = $searchForm->getSearchCriteria($searchVars);
+        if (is_null($criteria)) {
+            $notification->push(_("No search to export!"), 'horde.error');
+        } else {
+            $form = new ExportForm($vars);
+            $form->validate($vars);
+            if ($form->isValid()) {
+                $form->getInfo($vars, $info);
+                $hours = $hermes->getHours($criteria);
+                if (is_a($hours, 'PEAR_Error')) {
+                    $notification->push($hours, 'horde.error');
+                } elseif (is_null($hours) || count($hours) == 0) {
+                    $notification->push(_("No time to export!"), 'horde.error');
+                } else {
+                    $exportHours = Hermes::makeExportHours($hours);
+                    $data = Horde_Data::factory(array('hermes', $info['format']));
+                    $filedata = $data->exportData($exportHours);
+                    $browser->downloadHeaders($data->getFilename('export'), $data->getContentType(), false, strlen($filedata));
+
+                    echo $filedata;
+                    if (!empty($info['mark_exported']) &&
+                        $info['mark_exported'] == 'yes' &&
+                        $perms->hasPermission('hermes:review', Horde_Auth::getAuth(),
+                                              Horde_Perms::EDIT)) {
+                        $hermes->markAs('exported', $hours);
+                    }
+                    exit;
+                }
+            }
+        }
+    }
+}
+
+
+$title = _("Search for Time");
+$print_view = (bool)$vars->get('print');
+if (!$print_view) {
+    Horde::addScriptFile('popup.js', 'horde', true);
+}
+require HERMES_TEMPLATES . '/common-header.inc';
+
+if (isset($_SESSION['hermes_search_criteria'])) {
+    $searchVars = unserialize($_SESSION['hermes_search_criteria']);
+} else {
+    $searchVars = $vars;
+}
+$form = new SearchForm($searchVars);
+
+if ($print_view) {
+    require_once $registry->get('templates', 'horde') . '/javascript/print.js';
+} else {
+    $print_link = Horde::url(Horde_Util::addParameter('search.php', array('print' => 'true')));
+    require HERMES_TEMPLATES . '/menu.inc';
+    $form->renderActive(new Horde_Form_Renderer(), $searchVars, 'search.php', 'post');
+    echo '<br />';
+}
+
+if (isset($_SESSION['hermes_search_criteria'])) {
+    echo Hermes::tabs();
+
+    if (is_null($criteria)) {
+        $criteria = $form->getSearchCriteria($searchVars);
+    }
+
+    $table = new Horde_Ui_Table('results', $vars,
+                                array('title' => _("Search Results"),
+                                      'name' => 'hermes/hours',
+                                      'params' => $criteria));
+
+    $template = new Horde_Template();
+    $template->setOption('gettext', true);
+    $template->set('postUrl', Horde::applicationUrl('time.php', false, -1));
+    $template->set('sessionId', Horde_Util::formInput());
+    $template->set('table', $table->render());
+
+    echo $template->fetch(HERMES_TEMPLATES . '/time/form.html');
+}
+
+if (!$print_view) {
+    echo '<br />';
+    $exportForm = new ExportForm($vars);
+    $exportForm->renderActive(new Horde_Form_Renderer(), $vars, 'search.php', 'post');
+}
+
+require $registry->get('templates', 'horde') . '/common-footer.inc';
diff --git a/hermes/start.php b/hermes/start.php
new file mode 100644 (file)
index 0000000..07b7509
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+/**
+ * $Horde: hermes/start.php,v 1.14 2009/07/14 00:25:32 mrubinsk Exp $
+ *
+ * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ *
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ * @author Jan Schneider <jan@horde.org>
+ */
+
+@define('HERMES_BASE', dirname(__FILE__));
+require_once HERMES_BASE . '/lib/base.php';
+
+$vars = Horde_Variables::getDefaultVariables();
+
+$form = new Horde_Form($vars, _("Stop Watch"));
+$form->addVariable(_("Stop watch description"), 'description', 'text', true);
+
+if ($form->validate($vars)) {
+    $timers = $prefs->getValue('running_timers', false);
+    if (empty($timers)) {
+        $timers = array();
+    } else {
+        $timers = @unserialize($timers);
+        if (!$timers) {
+            $timers = array();
+        }
+    }
+    $now = time();
+    $timers[$now] = array('name' => Horde_String::convertCharset($vars->get('description'),
+                                                       Horde_Nls::getCharset(),
+                                                       $prefs->getCharset()),
+                          'time' => $now);
+    $prefs->setValue('running_timers', serialize($timers), false);
+
+    Horde_Util::closeWindowJS('alert(\'' . addslashes(sprintf(_("The stop watch \"%s\" has been started and will appear in the sidebar at the next refresh."), $vars->get('description'))) . '\');');
+    exit;
+}
+
+$title = _("Stop Watch");
+require HERMES_TEMPLATES . '/common-header.inc';
+
+$renderer = new Horde_Form_Renderer();
+$form->renderActive($renderer, $vars, 'start.php', 'post');
+
+require $registry->get('templates', 'horde') . '/common-footer.inc';
diff --git a/hermes/templates/common-header.inc b/hermes/templates/common-header.inc
new file mode 100644 (file)
index 0000000..b64edd5
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+if (isset($language)) {
+    header('Content-type: text/html; charset=' . Horde_Nls::getCharset());
+    header('Vary: Accept-Language');
+}
+?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">
+<!-- Hermes: Copyright 2002-2009 The Horde Project. Hermes is under a Horde license. -->
+<!--   Horde Project: http://www.horde.org/ | Hermes: http://www.horde.org/hermes/    -->
+<!--                Horde Licenses: http://www.horde.org/licenses/                    -->
+<?php echo !empty($language) ? '<html lang="' . strtr($language, '_', '-') . '">' : '<html>' ?>
+<head>
+<?php
+
+$page_title = $GLOBALS['registry']->get('name');
+if (!empty($title)) $page_title .= ' :: ' . $title;
+if (!empty($refresh_time) && ($refresh_time > 0) && !empty($refresh_url)) {
+    echo "<meta http-equiv=\"refresh\" content=\"$refresh_time;url=$refresh_url\">\n";
+}
+
+Horde::includeScriptFiles();
+
+?>
+<title><?php echo htmlspecialchars($page_title) ?></title>
+<link href="<?php echo $GLOBALS['registry']->getImageDir()?>/favicon.ico" rel="SHORTCUT ICON" />
+<?php Horde::includeStylesheetFiles() ?>
+</head>
+
+<body<?php if ($bc = Horde_Util::nonInputVar('bodyClass')) echo ' class="' . $bc . '"' ?><?php if ($bi = Horde_Util::nonInputVar('bodyId')) echo ' id="' . $bi . '"'; ?>>
diff --git a/hermes/templates/deliverables/list.inc b/hermes/templates/deliverables/list.inc
new file mode 100644 (file)
index 0000000..bdcb85c
--- /dev/null
@@ -0,0 +1,13 @@
+<br />
+<table width="100%">
+<tr><td class="header"><?php
+    printf(_("%s Deliverables"), $clientname);
+
+    $newurl = Horde::applicationUrl('deliverables.php');
+    $newurl = Horde_Util::addParameter($newurl, array('new' => 1, 'client_id' => $vars->get('client_id')));
+
+?> <a href="<?php echo $newurl ?>"><?php
+    echo Horde::img('newdeliverable.png', _("New Top-level Deliverable"));
+?></a></td></tr>
+<tr><td><?php $tree->renderTree() ?></td></tr>
+</table>
diff --git a/hermes/templates/menu.inc b/hermes/templates/menu.inc
new file mode 100644 (file)
index 0000000..1864954
--- /dev/null
@@ -0,0 +1,4 @@
+<div id="menu">
+ <?php echo Hermes::getMenu('string') ?>
+</div>
+<?php $GLOBALS['notification']->notify(array('listeners' => 'status')) ?>
diff --git a/hermes/templates/time/form.html b/hermes/templates/time/form.html
new file mode 100644 (file)
index 0000000..07952fd
--- /dev/null
@@ -0,0 +1,6 @@
+<form method="post" action="<tag:postUrl />">
+<tag:sessionId />
+<input type="hidden" name="formname" value="submittimeform" />
+<tag:table />
+<input class="button" type="submit" name="submit" value="<gettext>Submit Selected Time</gettext>" />
+</form>
diff --git a/hermes/themes/graphics/clockout.png b/hermes/themes/graphics/clockout.png
new file mode 100644 (file)
index 0000000..2802414
Binary files /dev/null and b/hermes/themes/graphics/clockout.png differ
diff --git a/hermes/themes/graphics/deliverable.png b/hermes/themes/graphics/deliverable.png
new file mode 100644 (file)
index 0000000..2917a4c
Binary files /dev/null and b/hermes/themes/graphics/deliverable.png differ
diff --git a/hermes/themes/graphics/favicon.ico b/hermes/themes/graphics/favicon.ico
new file mode 100644 (file)
index 0000000..5379a75
Binary files /dev/null and b/hermes/themes/graphics/favicon.ico differ
diff --git a/hermes/themes/graphics/hermes.png b/hermes/themes/graphics/hermes.png
new file mode 100644 (file)
index 0000000..f691913
Binary files /dev/null and b/hermes/themes/graphics/hermes.png differ
diff --git a/hermes/themes/graphics/invoices.png b/hermes/themes/graphics/invoices.png
new file mode 100644 (file)
index 0000000..1f5084b
Binary files /dev/null and b/hermes/themes/graphics/invoices.png differ
diff --git a/hermes/themes/graphics/newdeliverable.png b/hermes/themes/graphics/newdeliverable.png
new file mode 100644 (file)
index 0000000..13accaa
Binary files /dev/null and b/hermes/themes/graphics/newdeliverable.png differ
diff --git a/hermes/themes/graphics/timer-start.png b/hermes/themes/graphics/timer-start.png
new file mode 100644 (file)
index 0000000..974ed56
Binary files /dev/null and b/hermes/themes/graphics/timer-start.png differ
diff --git a/hermes/themes/graphics/timer-stop.png b/hermes/themes/graphics/timer-stop.png
new file mode 100644 (file)
index 0000000..a14f92d
Binary files /dev/null and b/hermes/themes/graphics/timer-stop.png differ
diff --git a/hermes/themes/print/screen.css b/hermes/themes/print/screen.css
new file mode 100644 (file)
index 0000000..01902a6
--- /dev/null
@@ -0,0 +1,8 @@
+/**
+ * $Horde: hermes/themes/print/screen.css,v 1.1 2005/08/04 17:56:45 chuck Exp $
+ */
+
+/* Special handling for printed timesheets. */
+#approval {
+    display: block;
+}
diff --git a/hermes/themes/screen.css b/hermes/themes/screen.css
new file mode 100644 (file)
index 0000000..3dce9e0
--- /dev/null
@@ -0,0 +1,40 @@
+/**
+ * $Horde: hermes/themes/screen.css,v 1.2 2006/06/28 00:12:53 chuck Exp $
+ */
+
+/* Special handling for non-printed timesheets. */
+#approval {
+    display: none;
+}
+
+/* Time list tables */
+table.time {
+    width: 100%;
+    margin-bottom: 8px;
+    border-top: 1px solid #ddd;
+    border-left: 1px solid #ddd;
+}
+table.time th {
+    padding: 3px;
+    background: #e9e9e9;
+    border-right: 1px solid #ccc;
+    text-align: left;
+}
+table.time td {
+    padding: 3px;
+    border-right: 1px solid #ddd;
+    border-bottom: 1px solid #ddd;
+}
+table.time th.sortup {
+    background: #bbcbff url("graphics/za.png") center left no-repeat;
+    padding-left: 10px;
+}
+table.time th.sortdown {
+    background: #bbcbff url("graphics/az.png") center left no-repeat;
+    padding-left: 10px;
+}
+table.time tfoot td {
+    background: #ddd;
+    font-weight: bold;
+    padding: 1px;
+}
diff --git a/hermes/time.php b/hermes/time.php
new file mode 100644 (file)
index 0000000..01f0e93
--- /dev/null
@@ -0,0 +1,87 @@
+<?php
+/**
+ * $Horde: hermes/time.php,v 1.55 2009/12/10 17:42:31 jan Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
+ *
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ */
+
+@define('HERMES_BASE', dirname(__FILE__));
+require_once HERMES_BASE . '/lib/base.php';
+require_once HERMES_BASE . '/lib/Forms/Time.php';
+require_once HERMES_BASE . '/lib/Table.php';
+
+$vars = Horde_Variables::getDefaultVariables();
+
+$delete = $vars->get('delete');
+if (!empty($delete)) {
+    $result = $hermes->updateTime(array(array('id' => $delete, 'delete' => true)));
+    if (is_a($result, 'PEAR_Error')) {
+        Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+        $notification->push(sprintf(_("There was an error deleting the time: %s"), $result->getMessage()), 'horde.error');
+    } else {
+        $notification->push(_("The time entry was successfully deleted."), 'horde.success');
+        $vars->remove('delete');
+    }
+}
+
+switch ($vars->get('formname')) {
+case 'submittimeform':
+    $time = array();
+    $item = $vars->get('item');
+    if (is_null($item) || !count($item)) {
+        $notification->push(_("No timeslices were selected to submit."),
+                            'horde.error');
+    } else {
+        foreach ($item as $id => $val) {
+            $time[] = array('id' => $id);
+        }
+        $result = $hermes->markAs('submitted', $time);
+        if (is_a($result, 'PEAR_Error')) {
+            $notification->push(sprintf(_("There was an error submitting your time: %s"), $result->getMessage()), 'horde.error');
+        } else {
+            $notification->push(_("Your time was successfully submitted."),
+                                'horde.success');
+            $vars = new Horde_Variables();
+        }
+    }
+    break;
+}
+
+// We are displaying all time.
+$tabs = Hermes::tabs();
+$criteria = array('employee' => Horde_Auth::getAuth(),
+                  'submitted' => false,
+                  'link_page' => 'time.php');
+$table = new Horde_Ui_Table('week', $vars,
+                            array('title' => _("My Unsubmitted Time"),
+                                  'name' => 'hermes/hours',
+                                  'params' => $criteria));
+
+$template = new Horde_Template();
+$template->setOption('gettext', true);
+$template->set('postUrl', Horde::applicationUrl('time.php', false, -1));
+$template->set('sessionId', Horde_Util::formInput());
+$template->set('table', $table->render());
+
+$title = _("My Time");
+$print_view = (Horde_Util::getFormData('print') == 'true');
+if (!$print_view) {
+    Horde::addScriptFile('popup.js', 'horde', true);
+}
+require HERMES_TEMPLATES . '/common-header.inc';
+
+if ($print_view) {
+    require $registry->get('templates', 'horde') . '/javascript/print.js';
+} else {
+    $print_link = Horde_Util::addParameter(Horde::applicationUrl('time.php'), 'print', 'true');
+    require HERMES_TEMPLATES . '/menu.inc';
+}
+
+echo $tabs;
+echo $template->fetch(HERMES_TEMPLATES . '/time/form.html');
+require $registry->get('templates', 'horde') . '/common-footer.inc';