# PRD — timesheet

Evidence date: 2026-10-01.

Source: `git@bitbucket.org:overdrive/timesheet.git`, `master` at `3baa25ba` (2025-07-16).

There is no prior PRD in the source repository or in Silicon Ark. Requirements below are recovered from the implementation. Provenance is labelled. Secrets in `system.php`, `fim_2.0/fim.config_*.php`, and the login PIN comparison are not reproduced.

## Product purpose

**Documented.** `README.md` titles the repository Timesheet.

**Implemented.** Staff record time against a client, project, and project item, optionally against a ticket item. The application totals hours and amount, compares them with monthly targets, supports invoicing and exports, and can push a time row to eIT and to Convergence.

**Unknown / Requires Confirmation:** the business owner, and whether Timesheet is still the system of record or a feeder into eIT and Convergence.

## Users and roles

| Actor | Evidence | Status |
|---|---|---|
| Person at the login screen | `login.php`. Email, password, External Login Pin | **Implemented** |
| Signed-in user | Session `USER` with `ID`, `USERNAME`, `EMAIL`, `SECURITYGROUPID` | **Implemented** |
| Security group | `sysUser.refSecurityGroupID` joined to `sysSecurityGroup`. Group must be active to log in | **Implemented**. Group names are data, not code |
| Page permission | `sysSecurity` flags `blnView`, `blnNew`, `blnSave`, `blnDelete`, `blnSpecial` per menu URL | **Implemented**. Which groups have which flags is **Unknown** |
| Inactive user | `sysUser.blnActive != 1` cannot log in | **Implemented** |
| Locked-out user | Five consecutive "Incorrect password" results set `blnActive = 0` and email a notice | **Implemented** in `CheckSecurityLock` |
| Cron | `cron.reminder.php` and `cron.daily.php` include the framework and do not show a login form | **Implemented**. How they are invoked is **Unknown** |
| Deploy operator | Pipeline and CodeDeploy hooks | **Inferred** from those files |

Menu labels are not in source. `NemoMenu::BuildMenu` reads `sysMenuLevel1` and `sysMenuLevel2` for the signed-in security group, and only shows a level-2 item when `blnView = 1` and `blnMenuItem = 1`. A page with `blnView != 1` redirects to `message.php` (`MID=r09`, or `e10` after more than five attempts in the session).

## Systems the product talks to

1. The browser, over PHP pages, form posts, and `ajaxfunctions.php`.
2. MySQL selected by `$_SERVER["SERVER_NAME"]` in `system.php`.
3. eIT at `https://eit.overdrive.co.za/ajax.functions.php` (`Timesheet::CopyToEit`).
4. Convergence at `https://convergence.overdrive.co.za/ajaxFunctions.php` (`SaveConvergenceTime`).
5. QuickBooks helper at `https://qb.overdrive.co.za/auth/` (`timesheet.php` action `QB Invoice`). `report.php` sets `$WebSeviceURL` to `https://qb.overdrive.co.za`.
6. SMTP, using `sysSettings` keys `SMTP Send As` and, for overrun mail, `SMTP CC`. The transport class is `_nemo.email.cls.php`. PHPMailer is required in the deploy hook, not in a committed `composer.json`.
7. File sessions under the host's `DOCUMENT_ROOT/SESSION`. That directory name is in `.gitignore`.
8. AWS S3 on pipeline success. FIM emails `grp-fim@overdrive.co.za`.

## Major workflows

### WF-1 — Log in

`login.php` clears `USER` and `PAGES` from the session, then shows Email, Password, External Login Pin, Login, and Forgot Password.

On `Action=Login`:

1. Load `sysUser` joined to `sysSecurityGroup` where `strEmail` matches and `sysSecurityGroup.blnActive = 1`.
2. If no row: log "Login failed: User not found or SG not active". Message: "Login Details are invalid."
3. If `blnActive != 1`: log "Login failed: Inactive User". Message: "User account is inactive."
4. If active: `NemoPassword::checkPassword("User", UserID, password)` must succeed and the posted External Login Pin must equal the constant in `login.php`. The constant is not recorded here.
5. Success stores `UserID`, `strUser`, `strEmail`, and `refSecurityGroupID`, logs "Login successful", and redirects to `index.php`.
6. Failure logs "Login failed: Incorrect password" and calls `CheckSecurityLock`.

`Action=Redirect` loads a user by email and `strPasswordMD5`, copies `strPassword` into the login password variable, then falls through to Login. `Action=RedirectKnowmore` loads a user by email only and does the same. Both still go through the password-hash check and the PIN check inside Login.

Client-side `Validate()` returns true without checking fields.

Provenance: **Implemented**.

### WF-2 — Reset a forgotten password

Forgot Password on `login.php` posts `type=SendResetEmail` to `ajaxfunctions.php`.

If `sysUser.strEmail` matches, the code creates a random password hash, saves it on `sysPassword` with date `2010-01-01`, and emails template "Reset Password". The link is `resetPassword.php?strEmail=...&ResetKey=<hash>`. Unknown emails return `0` and the page says the address does not exist.

`resetPassword.php` compares `ResetKey` with the latest `sysPassword.hash`. If it matches, and the new password is not in the last `PASSWORD_MaxHistory` hashes, it stores a new salt and SHA-256 hash and returns to `login.php`.

Provenance: **Implemented**.

### WF-3 — Land on the home page

`index.php` requires a session (`Nemo` redirects to `login.php` when `USER` is missing).

If the security row for `booking.php` has `blnView = 1`, it renders today's booking list. The class `Booking` is referenced here. A booking page file was not in the tree that was listed. **Unknown** whether `booking.php` exists only in the database menu.

If `usage.log.php` has `blnView = 1`, the page lists `sysLogin` rows from the last two days: date, result, username, system user, active flag, security group.

Provenance: **Implemented**.

### WF-4 — Record a timesheet line

`timesheet.php` lists `tblUserProjectItem` joined to project item, project, client, and user.

Default filters: start date is the first of the current month, end date is the last day, user defaults to the signed-in user, client defaults to all. Search matches invoice notes and internal notes. "Ignore dates" drops the date predicate.

Columns include date, client, project and item, user, invoice notes, time in and out, hours, amount (`hours * rate`), invoiced flag, last user, last edit, and `eit` (`Y` when `eitBillingID > 0`).

In the default sort, the list inserts a total row per date and a grand total. Rows are coloured by weekday CSS class `trColour` plus the day name.

A banner from `Timesheet::getUserOfficeTotal` shows office and personal billed hours and rand against targets for the current `Y-m`. A side panel `getTicketUpdater` is included. Its full behaviour was not line-traced. **Requires confirmation** if that panel is still used.

New and edit use `NemoDetails` bound to `tblUserProjectItem`. The form calculates hours from start and end unless "Override" is ticked. A no-bill block can insert a second row on save.

Save (`Timesheet::Save`):

- Writes posted fields. Start and end come from `tempStartTime` and `tempEndTime`.
- Ticket item `0` is stored as SQL `NULL`.
- If "no bill" is checked, inserts another `tblUserProjectItem` row using the no-bill project item's rate.
- Calls `SaveConvergenceTime`.
- If `eitBillingID` is already non-zero, calls `CopyToEit` again.
- If the line is tied to a ticket item, may send the overrun email (see business rules).

Delete removes the `tblUserProjectItem` row. It is not a soft delete.

`timesheet_ticket.php` is a second entry point with the same table and a similar form, including ticket-item lookup. **Inferred:** it is the ticket-oriented capture screen. Behavioural differences were not fully diffed.

Provenance: **Implemented**.

### WF-5 — Copy time to eIT

Actions "Copy To Eit" and "Bulk Copy To Eit". The single-row button is shown when the client has an `eitClientID` and the line's `eitBillingID` is empty or zero. The list button is shown when the filtered client has an `eitClientID`. The list condition uses `!= NULL || != 0`, which is true for almost every value, including zero. **Implemented** as written. Whether that was intended is **Requires Confirmation**.

`CopyToEit` GETs eIT `request=SaveTime` with client, user, date, hours, rate, notes, and fixed codes documented in `business-rules.md`. On success it stores the returned `BillingID` on `tblUserProjectItem.eitBillingID`.

### WF-6 — Push time to Convergence

Every successful `Timesheet::Save` calls `SaveConvergenceTime`. The call runs only when a Convergence task id is resolved from `tblTicketItem.refTaskID`, a hardcoded ticket-item map, or a hardcoded client-name map. Otherwise the function returns null and save still succeeds.

Provenance: **Implemented**.

### WF-7 — Work tickets

`ticket.php` lists tickets with filters: search, user (unassigned, my tickets, all, or one active user), project, status (`Open`, `Busy`, `On Hold`, `Closed`), priority (`High`, `Medium`, `Low`).

"My tickets" includes `refUserID` equal to the session user or `-1`. Unassigned is `refUserID = -1`.

Creating a project can also create a ticket. `Project::CreateProjectTicket` inserts `tblTicket` (priority Medium, status Open, assigned date today) and one `tblTicketItem` per project item, copying estimated hours and notes.

Marking a ticket item complete sets `tblTicketItem.blnCompleted = 1` (`TicketItemComplete`, action `Completed` on `timesheet.php`).

Open ticket items for the timesheet dropdown are those whose ticket status is not `Closed` and `blnCompleted = 0`, plus the item already selected.

Provenance: **Implemented**.

### WF-8 — Clients, projects, items, templates, rates

Clients: list with search and active filter. Delete sets `blnActive = 0`. A client form can add or soft-delete projects.

Projects: belong to a client. Delete sets `blnActive = 0`. On create, a selected project template copies rows from `tblProjectItemTemplate` into `tblProjectItem` (`INSERT IGNORE`).

Project items belong to a project and can reference `tblRate`.

Rates: name, amount (`dblRate`), active flag. Delete behaviour is in `includes/rate.cls.php` (soft-delete pattern used by sibling admin classes; the delete SQL was not re-read line by line). **Requires Confirmation** if rate delete is hard or soft. List filter defaults to active.

SLA contracts: description, hours, start, end, active. List defaults to active.

Public holidays: `tblPublicHolidays` (`dtDate`, `strName`). Used by the reminder cron and by overview.

Provenance: **Implemented**, except the rate-delete note.

### WF-9 — Monthly overview

`overview.php` builds a `Target` for `YYYYMM` (default this month). Up and down image controls move one month.

Users included have `blnActive = 1` and `blnOverview = 1`. The class computes working days, holidays, billable and non-billable hours, leave, and yearly targets. The overview file is about 2,000 lines. This PRD does not restate every calculated column.

Provenance: **Implemented**. The exact on-screen columns need a live session to confirm. **Unknown** without rendering.

### WF-10 — Reports, invoice mark, IIF, QuickBooks

`report.php` offers:

| Label | File | Group |
|---|---|---|
| Timesheet Summary Report | `includes/timesheet.summary.rpt.php` | first |
| Timesheet Ticket Report | `includes/ticket.rpt.php` | first |
| Timesheet Report with Notes | `includes/timesheet.rpt.php` | first, and the default |
| Staff Totals Report | `includes/timesheet.totals.rpt.php` | first |
| Client Totals Report | `includes/client.totals.rpt.php` | first |
| Graph | `includes/graph.rpt.php` | second |
| Gantt Chart | `includes/gantt.rpt.php` | second |
| Dev Daily Report | `includes/dev.daily.rpt.php` | second |
| SLA Report | `includes/sla.rpt.php` | second |
| Recon Report | `includes/recon.rpt.php` | second |

Shared filters include client, project, project item, dates, user, invoiced, and ticket. Each report declares which of those it uses.

"Mark as Invoiced" sets `blnInvoiced = 1` on checked `tblUserProjectItem` rows (`Timesheet::markAsInvoiced`).

"IIF Export" builds rows in `includes/iifExport.cls.php` from selected timesheet ids, grouped by client. A second control is labelled "IFF Export Ticket" (spelling in code) and uses `includes/iifExportTicket.cls.php`.

"QB Invoice" on `timesheet.php` posts the current filtered list to `https://qb.overdrive.co.za/auth/` and redirects the browser to the URL returned in the JSON body. Line quantity is hardcoded to `1`. Unit price is the line amount. Item name is `Software`. Description source is invoice notes. `CustomerName` is the literal `A Import`.

Provenance: **Implemented**.

### WF-11 — Users, security, settings, email

Users are `sysUser`. Email is read-only after create. Password fields on the user form are hidden. An existing user gets a Reset Password button (`User::ResetPassword`). Export on the user list is hidden.

Security groups edit `sysSecurityGroup` and a grid of view, new, save, delete, and special checkboxes per menu level-2 item (`SecurityGroup::getSecurityEntities`).

Settings edit `sysSettings` except names like `vie%` (those are skipped when settings are loaded in `system.php`).

`email.php` lists stored mail, can update notes, resend, and send. Templates are `email.template.php` / `includes/email.template.cls.php`.

Provenance: **Implemented**.

### WF-12 — Daily reminder

`cron.reminder.php` uses timezone `Africa/Johannesburg`.

On Saturday or Sunday it prints that it is the weekend and stops.

On the last weekday of the month (Saturday and Sunday rolled back; public holidays are not applied in `lastWorkDayOfMonth`) it sets `sysUser.dblRemindCount = 0`.

On a public holiday (`tblPublicHolidays.dtDate` equals today) it stops.

Otherwise it emails active users whose summed hours are below `dblTarget / 20` and who have not already been reminded in the current window:

- Before 13:00: template `TimeEntryReminderMorning`, hours are for the previous working day (skips Saturday, Sunday, and holidays). CC `jaco@overdrive.co.za` and `gareth@overdrive.co.za`. If `dblRemindCount > 2`, also CC `olivia@overdrive.co.za`. Increments `dblRemindCount`.
- From 13:00: template `TimeEntryReminder`, hours are for today. A user already reminded today is included again only when `HOUR(dtLastReminded) < 16`.

`dtLastReminded` is updated whenever the send path runs.

Provenance: **Implemented**. Whether the script is on a crontab is **Unknown**.

## Functional requirements

### FR-001 — Authenticate before showing the app

Requirement: A page built with `Nemo` (not `NemoBasic`) redirects to `login.php` when `$_SESSION["USER"]` is missing.

Trigger: Any such page request.

Evidence: `_nemo.cls.php` constructor.

Implementation: Implemented.

Confidence: High.

### FR-002 — Enforce page view rights

Requirement: If the current script has no `sysSecurity.blnView = 1` for the user's security group, the user is sent to `message.php` and cannot see the page.

Evidence: `Nemo::iniSecurity`.

Implementation: Implemented.

Confidence: High.

### FR-003 — Record a time line

Requirement: A user who can open `timesheet.php` can create and update a `tblUserProjectItem` row with user, project item, date, start, end, hours, rate, invoice notes, and internal notes, and an optional ticket item.

Evidence: `timesheet.php`, `Timesheet::Save`.

Implementation: Implemented.

Confidence: High.

### FR-004 — Calculate hours from the clock range

Requirement: The form sets hours from start and end. Override lets the user type hours that differ from that range. On edit, if stored hours differ from the range rounded to one decimal, the override checkbox starts checked.

Evidence: `timesheet.php` (`jsCalcHours`, comment 2014-02-12).

Implementation: Implemented.

Confidence: High.

### FR-005 — Show month targets on the timesheet

Requirement: The timesheet shows office and personal billed hours and amount against `sysUser.dblTarget` and `sysSettings.intBaseRate` for the current year-month. Amount rows with `dblRate = 0` are excluded from the billed sums.

Evidence: `Timesheet::getUserOfficeTotal`.

Implementation: Implemented.

Confidence: High.

### FR-006 — Copy selected time to eIT

Requirement: Copy sends the line to eIT SaveTime and stores the returned billing id.

Evidence: `Timesheet::CopyToEit`.

Implementation: Implemented.

Confidence: High.

### FR-007 — Copy mapped time to Convergence on save

Requirement: Save attempts a Convergence SaveTime when a task id can be resolved.

Evidence: `SaveConvergenceTime`.

Implementation: Implemented.

Confidence: High.

### FR-008 — Email estimate thresholds once each

Requirement: At 50%, 75%, and 100% of `dblHoursEst`, email the ticket owner once per threshold using template "Project Item Over Run".

Evidence: `Timesheet::Save`, `SendOverRunEmail`.

Implementation: Implemented.

Confidence: High.

### FR-009 — Remind users who are under the daily target

Requirement: Weekday cron emails active users under `dblTarget/20`, skipping weekends and public holidays, with the morning and afternoon split in WF-12.

Evidence: `cron.reminder.php`.

Implementation: Implemented.

Confidence: High.

### FR-010 — Mark lines invoiced

Requirement: Checked report rows can be marked `blnInvoiced = 1`. The last-user string records `Timesheet::markAsInvoiced` and the session user name.

Evidence: `Timesheet::markAsInvoiced`.

Implementation: Implemented.

Confidence: High.

### FR-011 — Export and hand off invoices

Requirement: Timesheet list can export to Excel. Reports can export IIF. QB Invoice posts to `qb.overdrive.co.za` and redirects to the returned URL.

Evidence: `timesheet.php` actions `Export` and `QB Invoice`, `includes/iifExport.cls.php`.

Implementation: Implemented.

Confidence: High for the request shape. The QuickBooks service contract is **Unknown** beyond this POST.

### FR-012 — Administer reference data

Requirement: Clients, projects, project items, templates, rates, SLA contracts, public holidays, users, security groups, settings, and email templates have list and detail screens backed by the tables named in `technical-documentation.md`.

Evidence: matching `*.php` and `includes/*.cls.php` files.

Implementation: Implemented.

Confidence: High.

### FR-013 — Lock an account after repeated bad passwords

Requirement: Five latest attempts that are all "Incorrect password", with no later success in that window, set the user inactive and send a lock email.

Evidence: `CheckSecurityLock` in `system.functions.inc.php`. Limit is 5.

Implementation: Implemented.

Confidence: High.

### FR-014 — Deploy the tree to the timesheet vhost

Requirement: On `master`, Bitbucket zips the working tree and copies it to the production S3 bucket. CodeDeploy app spec, if used, installs into `/var/www/html/timesheet.overdrive.co.za/` as `www-data`.

Evidence: `bitbucket-pipelines.yml`, `appspec.yml`.

Implementation: Pipeline upload is Implemented. That CodeDeploy consumes that zip is **Unknown / Requires Confirmation**.

Confidence: High for the pipeline file. Medium for the live deploy path.

## Important constraints

- Host name selects the database. An unknown host has no `$DATABASE_SETTINGS` entry and the connection fails.
- Page security is data. Shipping the PHP file does not put it on the menu.
- Several integrations use fixed ids and fixed client names. Changing a client display name can stop the Convergence push.
- `ajaxfunctions.php` includes `system.php` and does not construct `Nemo`, so it does not run the page view check. **Requires Confirmation** whether every caller is already logged in.
- Credentials and a login PIN are compiled into PHP. See `technical-documentation.md`. Values are omitted here on purpose.

## Assumptions

None of the following is written down as a requirement. They are readings of the code:

- Billable hours sent to eIT equal actual hours, multiplier 1, location `OFF-SITE`, division `SOFTWARE`.
- A "day" of target for reminders is `dblTarget / 20`.
- Soft-deleted clients and projects (`blnActive = 0`) drop out of active dropdowns.
- The product's users are Silicon Overdrive staff, because reminder CC addresses are Overdrive mailboxes and the footer links to overdrive.co.za.

Confirm these before treating them as policy.
