CoolCMS API

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

$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.

$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
SelectBox

buildSupplyKindRegionVatMaps()

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: array}>

createComponentWorkGrids()

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|null

applyNewItemFields()

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}

flashSheetResult()

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
string

groupedSheets()

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
bool

itemErrorLine()

private itemErrorLine(WorkSheet $sheet, int|string $rowIndex, string $reason) : string
Parameters
$sheet : WorkSheet
$rowIndex : int|string
$reason : string
Return values
string

itemFieldValues()

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|null

itemsBySheetMap()

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>>

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|null

parseVatPercents()

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|null

persistSheetFields()

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
$sheet : WorkSheet
$rows : array<int|string, mixed>

rows sheets[<id>][items] from WorkGrid

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}|null

resolveCurrency()

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|null

resolveDefaultVatPercents()

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|null

resolveSupplyKindSellerCountryCode()

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|null

resultDetail()

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
string

serviceLabel()

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
string

sheetItemSupplyKindId()

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|null

sheetSummary()

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 $row by default)

Return values
string|null

summarizeItemsRaw()

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.

Return values
array{items: int, hours: string, uninvoiced: string, invoiced: string}
On this page

Search results