WorkListSheetsFormTrait uses trait:short
"Work sheets" tab of a job: left column with sheet groups, right column with the spreadsheet-like grid.
One WorkGrid instance per sheet via Multiplier, all in the DOM at once and JS only toggles
visibility, so switching sheets never loses values typed in other grids. All four sheet signals
(addSheet!, closeSheet!, reopenSheet!, deleteSheet!) are AJAX and redraw only the workListForm
snippet; see handleAddSheet() and the handlers below.
Single submit: persistSheetsAndItems() is called by
WorkListFormTrait::saveWorkListForm() after the job is
saved. It saves targeted, never replacing the collection: per sheet updateColumns() for name, bill and order;
per existing item updateColumns() of the editable fields, [deleted]=1 calls delete(), a new row (no [id],
with a title) calls save().
IMPORTANT: price is never sent (the server computes it) and [deleted] is never hydrated; it is translated
to the manager's delete().
Terminology: WorkList is a job, WorkSheet is a work sheet inside a job.
Table of Contents
Constants
- SUPPLY_KIND_SETTING_MODULE_INVOICER : mixed = 7
- Module "invoicer" in the settings table (`cms_system_settings.module`); holds the portal `sellerId`.
Properties
- $supplyKindFieldsManager : SupplyKindManager
- $supplyKindFieldsSellerManager : SellerManager
- $supplyKindFieldsVatRateManager : VatRateManager
- $forcedActiveSheetId : int|null
- Id of the sheet just created by the AJAX "add sheet"; `renderEdit()` activates it instead of the first sheet.
- $itemsBySheet : array<int, array<int, WorkItem>>|null
- $sheets : array<int, WorkSheet>|null
- $sheetsCurrency : Currency|null
- Resolved job currency ({@see resolveCurrency()}); `null` when the system has none.
- $sheetsCurrencyResolved : bool
- Sentinel: `$sheetsCurrency === null` alone does not tell whether it was resolved already.
- $sheetsDefaultVatPercents : string|null
- Effective default VAT percent ({@see resolveDefaultVatPercents()}); `null` when neither customer nor company has one.
- $sheetsDefaultVatPercentsResolved : bool
- Sentinel: `$sheetsDefaultVatPercents === null` alone does not tell whether it was resolved (0 % is a valid value).
- $sheetServiceOptions : array<int, string>|null
- $sheetsTabActive : bool
- After the AJAX "add sheet" the "Work sheets" tab stays active; otherwise the redrawn snippet would show the default job tab.
- $sheetSupplyKindOptions : array<int, string>|null
Methods
- handleAddSheet() : void
- Creates a sheet immediately so the grid has a `sheetId` for its row name prefix.
- handleCloseSheet() : void
- Manually closes a sheet (`open` to `closed`); an ordinary action without a special privilege.
- handleDeleteSheet() : void
- Deletes a sheet (only an empty or open one); mirrors the server guard, which enforces it anyway.
- handleReopenSheet() : void
- Reopens a sheet (`closed|invoiced` to `open`); a privilege the server enforces too.
- addSupplyKindSelect() : SelectBox
- Adds the supply kind select. The options are database names, so the select translator is off and the prompt is translated here. IMPORTANT: the label must stay a translation key (`BaseControl::getLabel()` takes the translator from the form); a translated label would be translated twice.
- buildSupplyKindRegionVatMaps() : array<int, array<int, string>>
- Returns the rate map for the helper field "price with VAT": `regionId => supplyKindId => percent` (valid today).
-
buildSupplyKindVatMapAttributes()
: array<int, array{json: string, map: array
}> - Returns the same as {@see buildSupplyKindRegionVatMaps()} plus JSON for the `data-price-vat-map` attribute of the region matrix (keys as strings, because JS reads `map[select.value]`).
- createComponentWorkGrids() : Multiplier<string|int, WorkGrid>
- Multiplier with one {@see WorkGrid} instance per sheet (`{control workGrids-<sheetId>}`).
- getSupplyKindOptions() : array<int, string>
- Returns active supply kinds as `id => name` in the admin language.
- supplyKindIdValue() : int|null
- Converts the supply kind select value to an id, or null for the empty prompt.
- applyNewItemFields() : void
- Sets the editable fields of a new entity directly from a POSTed row with typed setters.
- billSelectionSummary() : array{count: int, price: string}
- Returns the initial state of the invoicing box: count and total price of sheets with `bill=1`.
- findSheet() : WorkSheet|null
- flashSheetResult() : void
- formatPrice() : string
- Formats an amount in the resolved job currency ({@see resolveCurrency()}).
- groupedSheets() : WorkSheet>}
- Splits sheets into the three aside groups.
- isSheetReadOnly() : bool
- Tells whether the sheet is locked itself or the whole job is locked (archive guard).
- itemErrorLine() : string
- itemFieldValues() : array<string, mixed>
- Returns the editable item fields from a POSTed row.
- itemNumberError() : string|null
- Validates all numeric cells of a row against the real column limits.
- itemsBySheetMap() : array<int, array<int, WorkItem>>
- Loads the work items of all sheets of the job in one query (`workSheetId IN (...)`), so opening a job with N sheets does not turn into N queries.
- itemsForSheet() : array<int, WorkItem>
- loadSheets() : array<int, WorkSheet>
- Returns the non-deleted sheets of the current job in `order`.
- parseDecimal() : numeric-string|null
- Converts free text to a normalized decimal string (comma to dot, empty to null), or `null` for invalid input.
- parseVatPercents() : string|null
- Converts free text to a VAT rate 0-100 (decimals allowed), or `null`.
- persistSheetFields() : array<int, string>
- Renames a sheet or changes its `bill` flag and order, with a targeted `updateColumns()` (no collection replace).
- persistSheetItems() : array<int, string>
- persistSheetsAndItems() : array<int, string>
- Called by {@see WorkListFormTrait::saveWorkListForm()} after the job is saved.
- prepareServiceOptions() : array<int, string>
- Returns service options of the job's company only (not of all companies in the system).
- prepareSheetSupplyKindOptions() : array<int, string>
- Returns supply kind options for the item grids: active kinds plus kinds already used by items of the job.
- priceFormatAttributes() : array{sep: string, dec: string, precision: int, prefix: string, suffix: string}|null
- Returns the currency format configuration for the JS live recalculation of the invoicing box.
- resolveCurrency() : Currency|null
- Resolves the currency of the "Work sheets" tab.
- resolveDefaultVatPercents() : string|null
- Resolves the effective default VAT rate, the placeholder of the `vatPercents` field in the work item grid.
- resolveSupplyKindSellerCountryCode() : string|null
- Returns the portal seller country (`7/invoice.default.sellerId`) as a fallback for a region without a country; null when the seller is not found.
- resultDetail() : string
- Returns the failure detail for `%message%` in flash translations.
- serviceLabel() : string
- Returns the service name in the current language (falling back to the first non-empty one, else `#id`).
- sheetErrorLine() : string
- sheetItemSupplyKindId() : int|null
- Resolves the supply kind from a POSTed grid row (raw POST, no Nette validation).
- sheetSummary() : array{count: int, hours: string, price: string, priceRaw: float}
- Returns the sheet summary.
- sheetTextError() : string|null
- Returns the error of a text cell longer than its column holds (`varchar` in characters, `TEXT` in bytes, measured on the real column, {@see \App\Core\Utils\Schema\ColumnSchema}), or `null`. Without it an oversized sheet name or item title / description failed the write in the database and only the generic row error was shown.
- summarizeItemsRaw() : array{count: int, hours: float, price: float}
- Summarizes work items: count, hours and price.
- unreadCommentCounts() : array<int, int>
- Returns unread client comment counts for the given work items, with one query per sheet.
- workListSummary() : array{items: int, hours: string, uninvoiced: string, invoiced: string}
- Returns the summary of the whole job (invoicing box): total items, total hours, uninvoiced and invoiced amounts.
Constants
SUPPLY_KIND_SETTING_MODULE_INVOICER
Module "invoicer" in the settings table (`cms_system_settings.module`); holds the portal `sellerId`.
private
mixed
SUPPLY_KIND_SETTING_MODULE_INVOICER
= 7
Properties
$supplyKindFieldsManager
public
SupplyKindManager
$supplyKindFieldsManager
Attributes
- #[Inject]
$supplyKindFieldsSellerManager
public
SellerManager
$supplyKindFieldsSellerManager
Attributes
- #[Inject]
$supplyKindFieldsVatRateManager
public
VatRateManager
$supplyKindFieldsVatRateManager
Attributes
- #[Inject]
$forcedActiveSheetId
Id of the sheet just created by the AJAX "add sheet"; `renderEdit()` activates it instead of the first sheet.
private
int|null
$forcedActiveSheetId
= null
null means no change.
$itemsBySheet
private
array<int, array<int, WorkItem>>|null
$itemsBySheet
= null
memoized itemsBySheetMap(), keyed by workSheetId
$sheets
private
array<int, WorkSheet>|null
$sheets
= null
memoized loadSheets()
$sheetsCurrency
Resolved job currency ({@see resolveCurrency()}); `null` when the system has none.
private
Currency|null
$sheetsCurrency
= null
$sheetsCurrencyResolved
Sentinel: `$sheetsCurrency === null` alone does not tell whether it was resolved already.
private
bool
$sheetsCurrencyResolved
= false
$sheetsDefaultVatPercents
Effective default VAT percent ({@see resolveDefaultVatPercents()}); `null` when neither customer nor company has one.
private
string|null
$sheetsDefaultVatPercents
= null
$sheetsDefaultVatPercentsResolved
Sentinel: `$sheetsDefaultVatPercents === null` alone does not tell whether it was resolved (0 % is a valid value).
private
bool
$sheetsDefaultVatPercentsResolved
= false
$sheetServiceOptions
private
array<int, string>|null
$sheetServiceOptions
= null
memoized prepareServiceOptions()
$sheetsTabActive
After the AJAX "add sheet" the "Work sheets" tab stays active; otherwise the redrawn snippet would show the default job tab.
private
bool
$sheetsTabActive
= false
$sheetSupplyKindOptions
private
array<int, string>|null
$sheetSupplyKindOptions
= null
memoized prepareSheetSupplyKindOptions()
Methods
handleAddSheet()
Creates a sheet immediately so the grid has a `sheetId` for its row name prefix.
public
handleAddSheet() : void
Attributes
- #[Requires]
- $methods: 'POST'
handleCloseSheet()
Manually closes a sheet (`open` to `closed`); an ordinary action without a special privilege.
public
handleCloseSheet(int $sheetId) : void
IMPORTANT: the parameter is named sheetId, not id: id is the persistent presenter parameter carrying the job.
A signal named id overrode it in the link (?id=<sheet>&do=...), the presenter then looked for a job with the
sheet's id and the action ended in a 404.
Parameters
- $sheetId : int
Attributes
- #[Requires]
- $methods: 'POST'
handleDeleteSheet()
Deletes a sheet (only an empty or open one); mirrors the server guard, which enforces it anyway.
public
handleDeleteSheet(int $sheetId) : void
Parameters
- $sheetId : int
Attributes
- #[Requires]
- $methods: 'POST'
handleReopenSheet()
Reopens a sheet (`closed|invoiced` to `open`); a privilege the server enforces too.
public
handleReopenSheet(int $sheetId) : void
IMPORTANT: the parameter is sheetId, not id; see handleCloseSheet().
Parameters
- $sheetId : int
Attributes
- #[Requires]
- $methods: 'POST'
addSupplyKindSelect()
Adds the supply kind select. The options are database names, so the select translator is off and the prompt is translated here. IMPORTANT: the label must stay a translation key (`BaseControl::getLabel()` takes the translator from the form); a translated label would be translated twice.
protected
addSupplyKindSelect(Form $form, string $name, string $labelKey, string|null $promptKey[, string|null $descriptionKey = null ][, int|null $currentId = null ]) : SelectBox
Parameters
- $form : Form
- $name : string
- $labelKey : string
- $promptKey : string|null
-
null = no prompt (the field is required, the first or current kind is preselected)
- $descriptionKey : string|null = null
- $currentId : int|null = null
Return values
SelectBoxbuildSupplyKindRegionVatMaps()
Returns the rate map for the helper field "price with VAT": `regionId => supplyKindId => percent` (valid today).
protected
buildSupplyKindRegionVatMaps(iterable<string|int, PaymentRegion> $regions) : array<int, array<int, string>>
Parameters
- $regions : iterable<string|int, PaymentRegion>
Return values
array<int, array<int, string>>buildSupplyKindVatMapAttributes()
Returns the same as {@see buildSupplyKindRegionVatMaps()} plus JSON for the `data-price-vat-map` attribute of the region matrix (keys as strings, because JS reads `map[select.value]`).
protected
buildSupplyKindVatMapAttributes(iterable<string|int, PaymentRegion> $regions) : array<int, array{json: string, map: array}>
For the template: regionId => {json, map}.
Parameters
- $regions : iterable<string|int, PaymentRegion>
Return values
array<int, array{json: string, map: arraycreateComponentWorkGrids()
Multiplier with one {@see WorkGrid} instance per sheet (`{control workGrids-<sheetId>}`).
protected
createComponentWorkGrids() : Multiplier<string|int, WorkGrid>
All sheets render at once (JS toggles visibility), so the form never loses rows typed in another sheet when switching.
Return values
Multiplier<string|int, WorkGrid>getSupplyKindOptions()
Returns active supply kinds as `id => name` in the admin language.
protected
getSupplyKindOptions([int|null $keepId = null ]) : array<int, string>
Parameters
- $keepId : int|null = null
Return values
array<int, string>supplyKindIdValue()
Converts the supply kind select value to an id, or null for the empty prompt.
protected
supplyKindIdValue(mixed $value) : int|null
Parameters
- $value : mixed
Return values
int|nullapplyNewItemFields()
Sets the editable fields of a new entity directly from a POSTed row with typed setters.
private
applyNewItemFields(WorkItem $item, array<int|string, mixed> $row) : void
It does not round-trip through itemFieldValues(), which returns array<string, mixed> for updateColumns(); here mixed would have to be cast back.
Parameters
- $item : WorkItem
- $row : array<int|string, mixed>
billSelectionSummary()
Returns the initial state of the invoicing box: count and total price of sheets with `bill=1`.
private
billSelectionSummary() : array{count: int, price: string}
JS then recalculates live from the ticked sheets; this is only the server-side default render.
Return values
array{count: int, price: string}findSheet()
private
findSheet(int $id) : WorkSheet|null
Parameters
- $id : int
Return values
WorkSheet|nullflashSheetResult()
private
flashSheetResult(Result $result, string $successKey, string $errorKey) : void
Parameters
- $result : Result
- $successKey : string
- $errorKey : string
formatPrice()
Formats an amount in the resolved job currency ({@see resolveCurrency()}).
private
formatPrice(float $price) : string
Uses PriceHelper::convertByLocalization(), which handles separators, precision and
symbol position. Without a currency (none in the system) the plain number_format with 2 decimals is kept,
because convertByLocalization() with a null currency returns an unformatted float, not a string.
Parameters
- $price : float
Return values
stringgroupedSheets()
Splits sheets into the three aside groups.
private
groupedSheets() : WorkSheet>}
closing is deliberately grouped with "closed": the intermediate state is not shown separately and behaves as closed.
Return values
WorkSheet>}isSheetReadOnly()
Tells whether the sheet is locked itself or the whole job is locked (archive guard).
private
isSheetReadOnly(WorkSheet $sheet) : bool
Parameters
- $sheet : WorkSheet
Return values
boolitemErrorLine()
private
itemErrorLine(WorkSheet $sheet, int|string $rowIndex, string $reason) : string
Parameters
- $sheet : WorkSheet
- $rowIndex : int|string
- $reason : string
Return values
stringitemFieldValues()
Returns the editable item fields from a POSTed row.
private
itemFieldValues(array<int|string, mixed> $row) : array<string, mixed>
price is never included (the server computes it); id, deleted and order as position are handled separately in persistSheetItems().
Parameters
- $row : array<int|string, mixed>
Return values
array<string, mixed>itemNumberError()
Validates all numeric cells of a row against the real column limits.
private
itemNumberError(array<int|string, mixed> $row) : string|null
Returns the message for the first bad cell, or null when the row is fine. The grid has no per-cell Nette
validation, and the database either fails an overflow with a generic error or truncates it, depending on
sql_mode.
IMPORTANT: without this gate hours=12345.678 passed and, with a non-strict sql_mode, the hours were truncated
to 9999.99 in the DB but the price was computed from the untruncated value. Stored hours and price then disagree and
CompanyInvoiceGenerator issues an invoice that fails the
quantity times rate equals price check.
A bad value rejects the row, not a silent zero, the same policy as the import.
Parameters
- $row : array<int|string, mixed>
Return values
string|nullitemsBySheetMap()
Loads the work items of all sheets of the job in one query (`workSheetId IN (...)`), so opening a job with N sheets does not turn into N queries.
private
itemsBySheetMap() : array<int, array<int, WorkItem>>
Return values
array<int, array<int, WorkItem>>itemsForSheet()
private
itemsForSheet(int $sheetId) : array<int, WorkItem>
Parameters
- $sheetId : int
Return values
array<int, WorkItem>loadSheets()
Returns the non-deleted sheets of the current job in `order`.
private
loadSheets() : array<int, WorkSheet>
The drag-and-drop order in the DOM is the source of truth; order only helps the first render after save.
Return values
array<int, WorkSheet>parseDecimal()
Converts free text to a normalized decimal string (comma to dot, empty to null), or `null` for invalid input.
private
parseDecimal(mixed $raw, string $property) : numeric-string|null
IMPORTANT: a plain is_numeric($value) ? $value : null accepts exponent notation: 1e1000 passed as a valid
number, (float) turned it into INF, number_format(INF, 2) into the string 'inf'; a strict sql_mode
fails such a save with a generic database error, a non-strict one stored 0.00, so the item was free without
any message. The shape is therefore checked by a regular expression over the expanded value and the result is
rounded to the column precision, so the gate measures what is written.
Parameters
- $raw : mixed
- $property : string
-
property of WorkItem the column scale is read from
Return values
numeric-string|nullparseVatPercents()
Converts free text to a VAT rate 0-100 (decimals allowed), or `null`.
private
parseVatPercents(mixed $raw) : string|null
An empty field means "by customer/company"; the generator resolves the chain. It is a custom parser, not
AdminFormHelpersTrait::intOrNull(), whose (int) $value !== 0 filter would turn 0 % (reverse charge) into NULL.
The grid has no per-cell Nette validation (no Form::RANGE), so an out-of-range value is silently clamped
to the bound: a nonsensical rate does not pass, and one cell does not fail saving the whole row.
Parameters
- $raw : mixed
Return values
string|nullpersistSheetFields()
Renames a sheet or changes its `bill` flag and order, with a targeted `updateColumns()` (no collection replace).
private
persistSheetFields(WorkSheet $sheet, array<int|string, mixed> $sheetData) : array<int, string>
bill goes through the internal admin bridge: #[ExternalReadonly] on the Api side blocks only external writes
(see the docblock of WorkSheet::$bill).
Parameters
- $sheet : WorkSheet
- $sheetData : array<int|string, mixed>
Return values
array<int, string>persistSheetItems()
private
persistSheetItems(WorkSheet $sheet, array<int|string, mixed> $rows) : array<int, string>
Parameters
Return values
array<int, string>persistSheetsAndItems()
Called by {@see WorkListFormTrait::saveWorkListForm()} after the job is saved.
private
persistSheetsAndItems() : array<int, string>
An archived job is skipped entirely: the server would reject the write by its guard anyway, this just avoids useless requests.
Return values
array<int, string> —ready (translated) error lines; an empty array means everything is OK
prepareServiceOptions()
Returns service options of the job's company only (not of all companies in the system).
private
prepareServiceOptions() : array<int, string>
Return values
array<int, string>prepareSheetSupplyKindOptions()
Returns supply kind options for the item grids: active kinds plus kinds already used by items of the job.
private
prepareSheetSupplyKindOptions() : array<int, string>
Kinds deactivated meanwhile are kept; otherwise the row would show an empty choice and saving would silently clear the kind.
Return values
array<int, string>priceFormatAttributes()
Returns the currency format configuration for the JS live recalculation of the invoicing box.
private
priceFormatAttributes() : array{sep: string, dec: string, precision: int, prefix: string, suffix: string}|null
work-list-sheets.js formatPrice() reads it from data-price-* on [data-invbox] (template
_invoice_box.latte), so the recalculation of ticked sheets uses the resolved job currency, like
formatPrice() in PHP.
null only when the system has no currency; the template then omits the data-price-* attributes and JS falls
back to formatNumber() (2 decimals).
Prefix and suffix (the currency shortcut) derive from the localization-to-currency format (PriceFormat):
symbol position ('b' before the amount, 'a' after it) and getShortcut(). Simplification: getShowSuffix()
controls here whether the shortcut is shown at all in the JS recalculation, whereas in PHP it controls only the
",-" suffix of whole amounts. JS does not replicate the full convertByLocalization() logic because the final
amount always arrives from the server after submit.
Return values
array{sep: string, dec: string, precision: int, prefix: string, suffix: string}|nullresolveCurrency()
Resolves the currency of the "Work sheets" tab.
private
resolveCurrency() : Currency|null
Preference chain: job, job customer, job company, system default currency (the first from findAll(), since
currencies have no isDefault flag). null only when the system has no currency at all; formatting then falls
back to number_format().
Memoized with the $sheetsCurrencyResolved sentinel, because the legitimate result "no currency" would otherwise
be recomputed on every call.
Return values
Currency|nullresolveDefaultVatPercents()
Resolves the effective default VAT rate, the placeholder of the `vatPercents` field in the work item grid.
private
resolveDefaultVatPercents() : string|null
Chain: job customer, job company (the job itself has no VAT field, only invoice type and currency, see
WorkListFormTrait::addBillingFields()). An empty grid field means this default; CompanyInvoiceGenerator
derives it again from the same chain, so the placeholder is only a visual hint, not a stored value.
Memoized with the $sheetsDefaultVatPercentsResolved sentinel, because 0 % (reverse charge) is a legitimate
result, see resolveCurrency().
Return values
string|nullresolveSupplyKindSellerCountryCode()
Returns the portal seller country (`7/invoice.default.sellerId`) as a fallback for a region without a country; null when the seller is not found.
private
resolveSupplyKindSellerCountryCode() : string|null
Return values
string|nullresultDetail()
Returns the failure detail for `%message%` in flash translations.
private
resultDetail(Result $result) : string
A technical failure has an empty message from the repository (the detail is only in the server log), so a dash is used, as in self::sheetErrorLine(), instead of an empty space after the colon.
Parameters
- $result : Result
Return values
stringserviceLabel()
Returns the service name in the current language (falling back to the first non-empty one, else `#id`).
private
serviceLabel(Service $service, int|null $currentLang) : string
Parameters
- $service : Service
- $currentLang : int|null
Return values
stringsheetErrorLine()
private
sheetErrorLine(WorkSheet $sheet, string $reason) : string
Parameters
- $sheet : WorkSheet
- $reason : string
Return values
stringsheetItemSupplyKindId()
Resolves the supply kind from a POSTed grid row (raw POST, no Nette validation).
private
sheetItemSupplyKindId(mixed $raw, int|null $currentId) : int|null
Empty means NULL (the company's default kind); an offered kind or the unchanged stored kind is kept; anything else keeps the stored kind.
Parameters
- $raw : mixed
- $currentId : int|null
Return values
int|nullsheetSummary()
Returns the sheet summary.
private
sheetSummary(WorkSheet $sheet) : array{count: int, hours: string, price: string, priceRaw: float}
priceRaw is an unformatted float for JS: work-list-sheets.js parseNumber reads it from data-sheet-price
instead of parsing the pre-formatted price back.
Parameters
- $sheet : WorkSheet
Return values
array{count: int, hours: string, price: string, priceRaw: float}sheetTextError()
Returns the error of a text cell longer than its column holds (`varchar` in characters, `TEXT` in bytes, measured on the real column, {@see \App\Core\Utils\Schema\ColumnSchema}), or `null`. Without it an oversized sheet name or item title / description failed the write in the database and only the generic row error was shown.
private
sheetTextError(class-string $entity, array<int|string, mixed> $row[, array<int, string> $properties = ['name'] ]) : string|null
Parameters
- $entity : class-string
- $row : array<int|string, mixed>
- $properties : array<int, string> = ['name']
-
properties checked (all keys of
$rowby default)
Return values
string|nullsummarizeItemsRaw()
Summarizes work items: count, hours and price.
private
summarizeItemsRaw(array<int, WorkItem> $items) : array{count: int, hours: float, price: float}
IMPORTANT: price sums only items with billable = 1. A non-billable item keeps its computed price
(WorkItemManager::computePrice() does not know billable; "free" is
expressed by the flag, not by a zero price), but it must stay out of every summary: the price under the sheet
name, the invoicing box and "uninvoiced" must match what will really go on the invoice
(WorkItemManager::findBillableForSheets()).
count and hours are not filtered: an unticked item was still worked on (the invoice preview also reports
items and billableItems as two different numbers).
Parameters
- $items : array<int, WorkItem>
Return values
array{count: int, hours: float, price: float}unreadCommentCounts()
Returns unread client comment counts for the given work items, with one query per sheet.
private
unreadCommentCounts(array<int, WorkItem> $items) : array<int, int>
The badge counts only client comments the admin has not seen yet; the company's own comments are never counted, otherwise the badge would light up right after sending a reply.
Parameters
- $items : array<int, WorkItem>
Return values
array<int, int>workListSummary()
Returns the summary of the whole job (invoicing box): total items, total hours, uninvoiced and invoiced amounts.
private
workListSummary() : array{items: int, hours: string, uninvoiced: string, invoiced: string}
A sheet's price counts as invoiced or not by whether the sheet is invoiced.