Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 46 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# InvoiceShelf Tasks and Projects

The official Tasks, Projects and Time Tracking module for InvoiceShelf 3.x. It adds projects hung
off a customer, tasks that move across a configurable Kanban board, time recorded by hand or by a
running timer, and invoicing that turns unbilled hours into a draft invoice from wherever the
work is: a task, a selection of tasks, a whole project, or the unbilled time page.
The official Tasks, Projects and Time Tracking module for InvoiceShelf 3.x. It adds two sidebar
entries, Projects and Tasks, and builds invoicing the way Invoice Ninja does it: pick the work
(a task, a selection of tasks, or a whole project) and land on a draft invoice, rather than
picking a customer first and hunting for what to bill.

The module is `AGPL-3.0-only`.

Expand All @@ -16,17 +16,45 @@ The module is `AGPL-3.0-only`.
## What it adds

- **Projects.** Name, optional customer, description, colour, status, default billable rate,
budget and due date. A project without a customer is internal and never reaches the billing
screen.
- **Tasks and a Kanban board.** Tasks belong to a project or stand alone against a customer, and
move across per-company task statuses with drag ordering.
- **Time tracking.** Manual time entries or a running timer, one per user per company, with a
header chip showing elapsed time.
- **Rate resolution.** Task rate, then the assignee's project rate, then the project default, then
the company default from module settings, written onto the time entry so a later rate change
never rewrites history.
- **Task to invoice.** Review unbilled billable entries, choose a grouping, and produce a draft
invoice through the host's own invoice endpoint.
budget and due date, reachable from its own sidebar entry. A project without a customer is
internal and never reaches the billing screens.
- **Tasks, with three views over one filter.** The Tasks screen is the module's sidebar root: a
view switcher moves between a sortable **List**, a **Board** with drag ordering across the
company's task statuses, and a **Week** timesheet, and the project, member and status filters
in the address bar survive every switch. A task also has its own page, with the time log
underneath it.
- **Time tracking, everywhere a task appears.** Start or stop a task's timer from its row in the
list, its card on the board, its own page, or the floating quick-start button that stays
reachable from any screen (it search-picks a task by name and starts or stops on it without
leaving the page you are on). Only one timer runs per user per company; starting a second one
offers to stop the first. A header chip shows the elapsed time and opens the running task.
- **The task time log.** A task's own page lists every interval logged against it, hand-entered
or from the timer: start, end, duration, description, billable, and who logged it. A row
already on an invoice is marked and its time, billable flag and task cannot change; its
description still can.
- **Rate resolution.** Task rate, then the assignee's project rate, then the project default,
then the company default from module settings, written onto the time entry so a later rate
change never rewrites history.
- **Invoicing from the work, not from a wizard.** "Invoice" on a task row, the bulk selection
bar, a task's own page, or a project's header prepares a draft invoice, one line per task, and
opens it on the host's own invoice edit screen, ready to review and send. A selection spanning
two customers or two currencies is refused with a clear message instead of guessing. The
**Unbilled time** page (linked from Reports and from the Projects header) answers the
month-end question across every project and customer at once, and is where a single entry can
still be left off an invoice on purpose.
- **Settings**, under **Company Settings → Tasks and Projects**: the default hourly rate, the
rounding increment and whether a stopped entry rounds to the nearest increment, up, or down,
the first day of the week, whether members see each other's time, whether creating a task
starts its creator's timer, whether an invoiced task locks against further edits, whether an
invoiced task drops off the board, and which parts of an invoice line an invoiced task writes
(a project heading, the task's own description, and each entry's date, time range, hours and
description). The module's own settings page under the module menu shows the current value of
every one of these next to a link to the form that edits them.
- **Abilities.** `view-project`, `create-project`, `edit-project` and `delete-project`;
`view-task`, `create-task`, `edit-task`, `delete-task` and `manage-task-status`; `view-own-time`,
`view-all-time` and `edit-all-time`; and `invoice-tasks`, which also requires the host's own
`create-invoice` and `edit-invoice` abilities, because invoicing a task ends on the host's
invoice edit page.

See [`specs/tasks-projects.md`](../specs/tasks-projects.md) in the private specs repository for the
full scope and data model.
Expand All @@ -36,8 +64,9 @@ full scope and data model.
1. Sign in as a super administrator and open **Administration → Modules**.
2. Pair the application with the InvoiceShelf marketplace if it is not already paired, then install
and enable **Tasks and Projects**.
3. Open **Company Settings → Tasks and Projects** to set the default hourly rate, rounding
increment, week start day, and whether non-owners may see other members' time.
3. Open **Company Settings → Tasks and Projects** to set the default hourly rate, the rounding
increment and direction, the first day of the week, who may see other members' time, and the
task and invoice-line behaviour described above.

## Disable and uninstall

Expand Down
3 changes: 2 additions & 1 deletion app/Application/InvoiceLineComposer.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ private function capped(array $heading, array $lines): string
while ($kept !== []) {
array_pop($kept);
$dropped = count($lines) - count($kept);
$note = implode("\n", [...$heading, ...$kept, 'and '.$dropped.' more entries']);
$summary = 'and '.$dropped.' more '.($dropped === 1 ? 'entry' : 'entries');
$note = implode("\n", [...$heading, ...$kept, $summary]);

if (mb_strlen($note) <= self::MAX_LENGTH) {
return $note;
Expand Down
28 changes: 21 additions & 7 deletions app/Support/ModuleRegistration.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public static function register(string $modulePath): void
*/
private static function registerMenu(): void
{
Registry::registerMenu('tasks-projects', [
Registry::registerMenu('tasks-projects.projects', [
'title' => 'tasksprojects::menu.projects',
'link' => '/admin/modules/tasks-projects/projects',
'icon' => 'FolderIcon',
Expand All @@ -42,7 +42,7 @@ private static function registerMenu(): void
'priority' => 40,
]);

Registry::registerMenu('tasks-projects.tasks', [
Registry::registerMenu('tasks-projects', [
'title' => 'tasksprojects::menu.tasks',
'link' => '/admin/modules/tasks-projects',
'icon' => 'ClipboardDocumentListIcon',
Expand Down Expand Up @@ -87,11 +87,7 @@ private static function settingsSchema(): array
'type' => FieldType::Select->value,
'label' => 'tasksprojects::settings.rounding_direction',
'default' => ModuleSettings::DEFAULT_ROUNDING_DIRECTION,
'options' => [
Rounding::NEAREST => 'tasksprojects::settings.rounding_nearest',
Rounding::UP => 'tasksprojects::settings.rounding_up',
Rounding::DOWN => 'tasksprojects::settings.rounding_down',
],
'options' => self::directionOptions(),
],
[
'key' => 'week_start',
Expand Down Expand Up @@ -144,6 +140,24 @@ private static function switchField(string $key): array
];
}

/**
* The rounding directions, already in the reader's language.
*
* The host translates a field's label and a section's title, but hands a
* select's options to the form as they were registered, so an option that
* named a translation key would reach the screen as the key itself.
*
* @return array<string, string>
*/
private static function directionOptions(): array
{
return [
Rounding::NEAREST => __('tasksprojects::settings.rounding_nearest'),
Rounding::UP => __('tasksprojects::settings.rounding_up'),
Rounding::DOWN => __('tasksprojects::settings.rounding_down'),
];
}

/** @return array<int, string> */
private static function roundingOptions(): array
{
Expand Down
Loading
Loading