CoolCMS API

WorkListPresenter extends BasePresenter
in package
uses WorkListFormTrait, WorkListSheetsFormTrait, WorkListArchiveFormTrait, WorkListInvoiceFormTrait, WorkListCommentsFormTrait, CustomerPickerTrait, PickerHostTrait, AdminEditFormComponentsTrait, AdminFormHelpersTrait

FinalYes

Jobs (`/admin/company/work-list`); the `show` grid adds PHP aggregation over all work sheets of a job ({@see loadWorkItemAggregates()}).

The edit shell is one {form editForm} with two tabs: "Job" (form, WorkListFormTrait) and "Work sheets" (aside of sheet groups plus one spreadsheet-like grid per sheet, WorkListSheetsFormTrait). Modals: archive/restore (WorkListArchiveFormTrait), issue invoice (WorkListInvoiceFormTrait) and work item comments (WorkListCommentsFormTrait).

A job knows only open and archived (archiving ends the job and is not an invoicing action); the invoicing lifecycle lives on the sheet (WorkSheetState). The read-only state of an archived job is enforced by the Api WorkListManager guard; here it is only mirrored (the whole "Work sheets" tab is disabled and the "Job" form is not rendered at all).

Terminology: WorkList is a job, WorkSheet is a work sheet inside a job.

Table of Contents

Constants

LOGIN_BACKLINK  : string = 'backlink'
Login page parameter with the key of the stored request to return to ({@see loginReturnArgs()}).
DEFAULT_ITEMS_PER_PAGE  : mixed = 20
Items per page of admin lists when the DB setting is missing.
FORM_BACK_SUBSECTION  : mixed = 'formBack'
Session subsection mapping a key to the previous page URL of edit forms.
SESSION_NAMESPACE  : mixed = 'admin'
BLOCKED_EXTENSIONS  : mixed = ['php', 'php3', 'php4', 'php5', 'php7', 'php8',...
Extensions that must not be uploaded into the web-served folder; otherwise an editor upload would mean running foreign code on the server. It is no content whitelist (the editor uploads what is needed), only a safeguard against executable files.
EDITOR_PAGE_LIMIT  : mixed = 500
Listing cap; the dialog is a suggester, not a paged list.
EDITOR_PAGE_TARGETS  : mixed = ['front:system:page:show']
Routes the editor may link to: text pages only.
EDITOR_UPLOAD_FOLDER  : mixed = 'editorUploads'
Editor root (relative to the `uploads/` root). Changing it to `''` would open the whole `uploads/` tree to the manager; the editor user could then reach (and delete) files of other modules such as products and avatars.
IMAGE_EXTENSIONS  : mixed = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', '...
Extensions the file manager shows as a preview instead of a document icon.
LOCALE_MAP  : mixed = ['cs' => 'cs_CZ', 'en' => 'en_US', 'sk' => 'sk_...
Maps the short language code to the full locale of the registered NEON sources.

Properties

$accessTokenValidator  : AccessTokenValidator
Signature and expiry alone are not enough: a revoked token (`jti` on the deny-list) that has not expired yet would keep the session logged in although the Api guard rejects the same token. Same contract as {@see \App\Core\Traits\Api\Presenters\JwtAuthTrait::checkJwtAuth()}, one implementation.
$adminFormActionsFactory  : FormActionsFactory
$adminLangNavFactory  : LangNavFactory
$adminLanguageManager  : LanguageManager
$adminPageManager  : AdminPageManager
$authManager  : AuthManager
$authStorageManager  : AuthStorageManager
$authUserManager  : UserManager
Admin `UserManager` for {@see UserManager::login()}. A module must not read through another module, so admin has its own `login()` that calls the same Api endpoint (`api/system/auth/login`).
$columnSchema  : ColumnSchema
Limits of the real database columns for {@see guardColumns()}.
$container  : Container
$customerManager  : CustomerManager
$deviceDetector  : DeviceDetector
$editorAutosaveScope  : EditorAutosaveScope
Scope of the editor autosave drafts, see {@see \App\Core\Security\EditorAutosaveScope}.
$editorLinkPageManager  : PageManager
NOTE: own property name. `$pageManager` already exists on presenters (`PagePickerTrait`), and two `#[Inject]` properties of the same name would collide in the trait composition.
$editorUploadManager  : FileSystemManager
NOTE: own property name, not `$fileSystemManager`.
$eventDispatcher  : EventDispatcher
$formSubmissionGuard  : FormSubmissionGuard
One-time submission tokens of "add" forms, see {@see guardFormSubmitOnce()}.
$jwtManager  : JwtManager
$languageContext  : LanguageContext
$languageManager  : LanguageManager
$localizationContext  : LocalizationContext
$localizationManager  : LocalizationManager
$loginDeviceCookie  : LoginDeviceCookie
Cookie of a known device for the password login throttle ({@see self::rememberLoginDevice()}).
$modalWindowFactory  : ModalWindowFactory
Injected through Nette presenter inject (after construction), not the constructor.
$picker  : string
Picker mode of a list; when not empty, the presenter renders its `show` grid as a selection modal (no add/edit/delete/checkboxes, each row has a select button) in the minimal `@picker.latte` layout.
$pickerExclude  : int
Id of the record that cannot be selected in picker mode (its select button is hidden).
$pickerExcludeSubtreeLft  : int
`lft`/`rgt` bounds of a whole subtree whose nodes cannot be selected in picker mode.
$pickerExcludeSubtreeRgt  : int
$presenterContext  : PresenterContext
$privilegeManager  : PrivilegeManager
$queryCacheContext  : QueryCacheContext
Query cache policy for the whole admin request; `startup()` disables it.
$routerDispatcher  : RouterDispatcher
$settingContext  : SettingContext
$settingManager  : SettingManager
$themeContext  : ThemeContext
$themeManager  : ThemeManager
$themeResolver  : DefaultThemeResolver
$translator  : ITranslator
$translatorSessionResolver  : Session
$userManager  : UserManager
$adminPageEntity  : AdminPage|null
$debugMode  : bool
$productionMode  : bool
$savedEntityId  : int|null
Id of the record just saved; set by the `$processData` callback in `createForm()` after a successful save.
$translatorDomain  : string
$commentedItem  : WorkItem|null
Work item whose comments are shown; kept for the modal form.
$companyManager  : CompanyManager
$countryManager  : CountryManager
$currencyManager  : CurrencyManager
$customerIdsWithAccount  : array<int, true>|null
Memoized set of customer ids that have a client account; see {@see customerIdsWithAccount()}.
$dataGridFactory  : DataGridFactory
$editedWorkList  : WorkList|null
Edited job (null when adding).
$forcedActiveSheetId  : int|null
Id of the sheet just created by the AJAX "add sheet"; `renderEdit()` activates it instead of the first sheet.
$invoiceManager  : InvoiceManager
$invoicePreviewError  : string
$invoicePreviewFailed  : bool
True when the preview failed to load; the modal then shows only the error, never a table.
$invoicePreviewSheetIds  : array<int, int>
$invoicePreviewSheets  : array<int, array<string, mixed>>
$invoicePreviewTotals  : array{items: int, billableItems: int, skippedItems: int, hours: string, total: string}
$invoiceTypeManager  : InvoiceTypeManager
$isLoggingIn  : bool
$itemsBySheet  : array<int, array<int, WorkItem>>|null
$pickerFactory  : PickerFactory
$serviceManager  : ServiceManager
$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
$theme  : Theme|null
$userPrivileges  : array<int, string>
$workGridFactory  : WorkGridFactory
$workItemAggregates  : array<int, array{count: int, hours: string, price: string}>|null
Memoized map of work item aggregates per job (over all its sheets); see {@see loadWorkItemAggregates()}.
$workItemCommentManager  : WorkItemCommentManager
$workItemManager  : WorkItemManager
$workListManager  : WorkListManager
$workSheetManager  : WorkSheetManager

Methods

__construct()  : mixed
actionAdd()  : void
actionEdit()  : void
beforeRender()  : void
createComponent()  : IComponent|null
flashFormOwnErrors()  : void
Flashes form-level errors (`$form->getOwnErrors()`) as `danger` messages.
flashRejectedUploads()  : void
Flashes one `warning` listing the files the Api rejected (type, dimensions, unreadable) after a successful save of a form with a dropzone ({@see UploadRejection}).
formatLayoutTemplateFiles()  : array<string|int, mixed>
Admin version of {@see \App\Core\Traits\Front\Presenters\ThemeInitTrait::formatLayoutTemplateFiles()}.
getAdminPageEntity()  : AdminPage|null
getContainer()  : Container
getFormBackUrl()  : string
Returns the stored previous page URL for the key, used by the `cancel` link in the template.
getModalWindow()  : ModalWindow
getRouterDispatcher()  : RouterDispatcher
getTranslator()  : ITranslator
guardFormSubmitOnce()  : void
Protects a form against a double submit: one rendered form saves at most once.
handleAddSheet()  : void
Creates a sheet immediately so the grid has a `sheetId` for its row name prefix.
handleArchive()  : void
Archives the job (`open` to `archived`); ends the job, it is not an invoicing action.
handleChangeLocale()  : void
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.
handleEditorCreateFolder()  : void
Creates a subfolder in `folder` (path relative to the editor root).
handleEditorDelete()  : void
Deletes a file or (recursively) a folder. An empty path would point to the editor root, which cannot be deleted.
handleEditorFiles()  : void
Sends the folder listing for the editor file manager. The `folder` parameter is a path relative to the editor root (empty means the root).
handleEditorPages()  : void
Sends the page list for the link dialog as JSON `{"pages": [{id, label, url}, ...]}` sorted by label.
handleEditorRename()  : void
Renames a file or folder (`path` relative to the editor root).
handleEditorUpload()  : void
Accepts a file from the editor (field `file`, multipart POST; optional `folder` = subfolder of the editor root) and sends JSON `{"location": "/www/uploads/editorUploads/..."}`, the format TinyMCE expects from `images_upload_handler`.
handleInvoicePreview()  : void
Opens the modal with a read-only invoice preview (AJAX).
handleItemComments()  : void
Opens the modal with comments of one work item (AJAX).
handleLogout()  : void
handleReopenSheet()  : void
Reopens a sheet (`closed|invoiced` to `open`); a privilege the server enforces too.
handleSignOut()  : void
handleUnarchiveModal()  : void
Opens the "restore from archive" modal with a reason field (AJAX).
hasServiceByType()  : bool
Returns whether the container has a service of the given type.
initializeTranslator()  : void
isAddAction()  : bool
Tells whether the current action creates a record: `add` and the `add<Something>` actions (`addMenu`, `addKey`). Derived from the server context only, like {@see formSubmitPrivilege()}.
isAllowed()  : bool
Returns whether the logged-in user has the given privilege.
isSubmitOnceForm()  : bool
Tells whether the form was protected by {@see guardFormSubmitOnce()}.
loginWithCredentials()  : void
Logs in with username and password; called only by the login form (after validation and CSRF).
publishFieldErrors()  : void
Publishes element errors of a failed submit to the client.
redirectToFormBack()  : void
Redirects to the stored previous page and discards the key.
renderAdd()  : void
renderEdit()  : void
renderShow()  : void
`show` is the default action of admin presenters (route fallback).
resolveFormBackKey()  : string
Returns the previous-page key: from the URL if present (survives the update redirect), otherwise on a plain GET render of the edit page stores `Referer` in the session under a new random key.
runSubmitOnce()  : bool|null
Runs a saving step of a form under its one-time token (a form without the token just runs it).
setConfigParameters()  : void
setTranslator()  : $this
startup()  : void
typedComponent()  : T
Returns a subcomponent with a verified type.
authorize()  : void
Authorization hook; by default requires the `:Admin` privilege.
authorizeFormSubmit()  : void
Gate for writing presenter forms built by `createForm()`.
authorizeListing()  : void
Gate for reading lists.
checkAdminAccess()  : void
Entry gate to the admin: a guest is sent to login; a logged-in user without `:Admin` is sent to login with an error.
checkAuthentication()  : void
checkCurrentPrivilege()  : void
Builds the key from the current request (`:Admin:<Module>:<Presenter>:<action>`) and checks it. `getName()` already contains the `Admin:...` prefix, so only one leading colon is added.
checkPrivilege()  : void
Checks a specific privilege; throws 403 when denied.
createComponentAdminFormActions()  : FormActions
createComponentAdminLangNav()  : LangNav
createComponentCommentForm()  : Form
createComponentEditForm()  : Form
createComponentGrid()  : DataGrid
createComponentInvoiceConfirmForm()  : Form
createComponentModalWindow()  : ModalWindow
createComponentPicker()  : Multiplier<string|int, Picker>
createComponentUnarchiveForm()  : Form
createComponentWorkGrids()  : Multiplier<string|int, WorkGrid>
Multiplier with one {@see WorkGrid} instance per sheet (`{control workGrids-<sheetId>}`).
createForm()  : Form
Creates an edit form with the standard actions and a return to the previous page.
currentLangId()  : int|null
Returns the current language id (for picker labels etc.), null when unknown.
customerDeliveryPicker()  : PickerConfig
customerPicker()  : PickerConfig
domainTranslator()  : PrefixedTranslator|null
Returns a translator prefixed with the presenter domain, or `null` (empty domain or a translator other than Contributte).
formSubmitPrivilege()  : string
Returns the privilege key required to save a form built by `createForm()`.
getAdminItemsPerPage()  : int
Returns the default items per page of admin lists from the DB setting `admin.itemsPerPage` (module System = 1), falling back to {@see DEFAULT_ITEMS_PER_PAGE}.
getAdminLayout()  : string
Returns the layout name; in picker mode ({@see $picker}) the `show` grid uses the minimal `@picker.latte` (no sidebar/header) so the opener fetch returns only the table.
getFormBackFallback()  : string
Returns the return URL when the previous page is unknown; override in a presenter without a `show` action.
getFormLanguages()  : array<int, Language>
Returns the languages for multilingual form fields (admin `LanguageManager`).
getSessionKey()  : string
Returns a session key string, e.g. 'front.auth.step1'.
getSessionSection()  : SessionSection
Returns a Nette\SessionSection instance using a dynamically built session key.
guardColumns()  : void
Gates every text and number field of the form by the real column it writes to ({@see \App\Core\Forms\ColumnGate::guard()}): an oversized value is an error at the field, never a database error that closes the entity manager (admin walkthrough A1, A14).
initializePresenterContext()  : void
intOrNull()  : int|null
Converts mixed to int or null (an empty or zero value becomes null).
isSameSiteRequest()  : bool
Tells whether the request comes from the own site (Nette SameSite cookie); protects GET actions with a side effect.
isSupplyPlaceRelevant()  : bool
Tells whether the job involves work on immovable property (VAT place of supply).
loadUserPrivileges()  : void
Loads all privileges of the logged-in user with one database query. Call it right after `checkAdminAccess()` in `startup()` (the user is guaranteed to be logged in by then).
logout()  : void
parseDateTime()  : DateTime|null
Converts a `datetime-local` input or text value to `DateTime`, or null when empty or unparsable.
pickers()  : array<string, PickerConfig>
Returns the presenter's pickers: the customer (Invoicer `Customer`) with its own `PickerConfig` and a refresh submit button.
setTranslatorDomain()  : void
setupTemplateTranslator()  : void
Sets a translator prefixed with the presenter domain into the template (as Front components do, see {@see \App\Core\Base\BaseComponent::setup()}).
str()  : string
Safely converts a mixed form value to string.
strOrNull()  : string|null
Converts mixed to string or null (an empty string is stored as null).
addBasicFields()  : void
Adds company, name and internal note.
addBillingFields()  : void
Adds the "Invoicing" section: invoice type and currency directly on the job, both optional.
addClientFields()  : void
Adds the customer (picker, hidden `invoicerCustomerId`) and the customer contact (select).
addPickerRefreshButtons()  : void
Adds the hidden `customerRefresh` submit (`validationScope([])`, class `ajax`, `data-picker-refresh`), which `picker.js` clicks after a selection.
applyCustomerRefresh()  : void
Handles the customer refresh.
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`.
buildCommentsModalContent()  : string
buildContactOptions()  : array<int, string>
Builds customer contact options as `id => "name2/street, city (email)"` (only the parts the contact has).
buildInvoiceModalContent()  : string
buildUnarchiveModalContent()  : string
buildWorkListEntity()  : WorkList
Builds the job entity from form values.
contactLabel()  : string
createIdentity()  : SimpleIdentity|null
createMultiplierComponent()  : Multiplier<string|int, IComponent>
Creates a multiplier for the component.
createNewComponentInstance()  : IComponent|null
Creates a new component instance without adding it to the presenter; used by the multiplier.
createStandardComponent()  : IComponent|null
Creates a standard component (not a multiplier).
customerDeliveryFieldMap()  : array<string, string>
customerFieldMap()  : array<string, string>
Returns the mapping data key (from {@see customerPrefillData()}) to billing field. Shared by the picker (client `fieldMap`) and the server-side prefill (snippet redraw after a pick).
customerIdsWithAccount()  : array<int, true>
Returns ids of customers that have a client account (`Customer.userId`).
customerLabel()  : string
customerPrefillData()  : array<string, string>
editorBreadcrumb()  : array<int, array{label: string, path: string}>
Builds the breadcrumb navigation: cumulative segment paths.
editorLinkLangId()  : int
Returns the language used to pick labels and URLs (default 1, Czech).
editorPageLabel()  : string
Returns the page label: title, otherwise name, otherwise `#id`.
editorPageRoute()  : Route|null
Returns the first page route the editor may link to. A page may have several (many-to-many); the whitelisted one is taken, not the first in order, so a page with both `Page:show` and `Page:form` ends up on the right one.
editorPageUrl()  : string|null
Returns the front end address of a page. The router builds it from the page route, so `/o-nas` and any prefix come out on their own.
editorParentFolder()  : string|null
Returns the parent folder (null in the editor root).
editorPath()  : string|null
Converts a client path (relative to the editor root) to an Api path (relative to the `uploads/` root).
editorUploadName()  : string|null
Builds a safe and unique file name. Sanitization copies the Api layer rules (webalize of the base and of the extension), so the name returned in the URL is exactly the name the file is stored under. The random suffix prevents collisions: `writeFile()` overwrites the same name without asking, so two different `obrazek.jpg` files would overwrite each other.
editorUploadUrl()  : string
Returns the public URL of a file. `$path` is relative to the `uploads/` root (so it includes `editorUploads/`), the same shape as other uploads in the system.
ensureEditorRoot()  : void
Creates the editor root folder; when it already exists the Api returns false and nothing happens.
expireIdleLogin()  : bool
Returns whether the login expired from inactivity. If so, revokes the tokens (like a logout), clears the `auth` section, logs out the Nette identity and returns true; otherwise moves the activity stamp forward.
findCommentedItem()  : WorkItem|null
Returns the work item by id, or `null` when it does not exist or is outside the edited job.
findSheet()  : WorkSheet|null
flashSheetResult()  : void
formatPrice()  : string
Formats an amount in the resolved job currency ({@see resolveCurrency()}).
getClientIp()  : string
groupedSheets()  : WorkSheet>}
Splits sheets into the three aside groups.
initializeMetaTags()  : void
Fills the template with the title, meta tags and HTML text of the page from the translation (`PageText`) of the current language, for the layout (like the Front {@see \App\Core\Traits\Front\Presenters\PageInitTrait}).
initializePage()  : void
isDevelopment()  : bool
isItemInWorkList()  : bool
Tells whether the work item belongs, via its work sheet, to the given job.
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>
loadCompanyForSupplyPlace()  : Company|null
Loads the job's company for its default supply kind; `null` when the job has none yet.
loadCustomer()  : Customer|null
Loads the customer including contacts for the server-side prefill of the invoice snapshot.
loadLocalization()  : Localization|null
loadSheets()  : array<int, WorkSheet>
Returns the non-deleted sheets of the current job in `order`.
loadWorkItemAggregates()  : array<int, array{count: int, hours: string, price: string}>
Aggregates work items per job (count, total hours, total price over all its sheets).
localizationCurrencyKey()  : string|null
Admin ignores the localization currency (it takes the language only).
loginReturnArgs()  : array<string, string>
Remembers the request of a logged-out user so the login can return to it ({@see storeRequest()}, `backlink` of the login page, restored by `AuthStorageTrait::redirectAfterLoginAction()`).
logoutNetteUser()  : void
parseDecimal()  : numeric-string|null
Converts free text to a normalized decimal string (comma to dot, empty to null), or `null` for invalid input.
parsePickerName()  : array{0: string, 1: list}
Splits a picker instance name into the config key plus row indexes (multiplier and nested multiplier). `parentId` -> `['parentId', []]`; `address_2` -> `['address', ['2']]`; `parameter_1_0` -> `['parameter', ['1','0']]` (nested).
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.
prepareCompanyOptions()  : array<int, NotTranslate>
Returns company options as `id => internal name` (`Company.name` is a flat column, untranslated).
prepareCurrencyOptions()  : array<int, NotTranslate>
The name is already translated DB data, not a key, hence `NotTranslate`.
prepareCustomerOptions()  : array<int, string>
Returns customer options as `id => name` (via `$this->customerManager` injected by {@see CustomerPickerTrait}), only for the grid column.
prepareInvoiceTypeOptions()  : array<int, NotTranslate>
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.
processComment()  : void
Saves a comment. The server sets author and time; only the text comes from the form.
processFailedLogin()  : void
processInvalidToken()  : void
processInvoiceConfirm()  : void
processLogin()  : void
processLoginException()  : void
processMissingToken()  : void
processSuccessfulLogin()  : void
processUnarchive()  : void
processValidToken()  : void
readLoginDeviceToken()  : string|null
Returns the raw known-device cookie for the Api login (the Api verifies its signature).
redirectAfterLoginAction()  : void
After a login attempt: a successful login returns to the request remembered before the login (`backlink`, {@see PrivilegeCheckTrait::loginReturnArgs()}), otherwise to the dashboard; a failed one stays on the login page and keeps the way back.
refreshNetteIdentity()  : bool
Refreshes the token of the same logged-in user: identity data (token, name, email) are overwritten in the stored object without `login()`, so neither the session id nor the form CSRF tokens change. The identity is stored in the session section as an object; the change is written at the end of the request.
rememberLoginDevice()  : void
After a successful login (password or OAuth) issues or renews the known-device cookie ({@see LoginDeviceCookie}): a signed proof that this browser has already logged in to the account. The next login sends it to the Api ({@see self::processLogin()}), and above the account limit the password from this browser is still verified, so foreign attempts do not lock out the owner. HttpOnly, SameSite=Lax, Secure on HTTPS; lifetime from `ratelimit.loginDevice.cookieLifetime`.
resolveAdminAuthorName()  : string
Returns the logged-in admin's display name; the fallback keeps `author_name` non-empty.
resolveCurrency()  : Currency|null
Resolves the currency of the "Work sheets" tab.
resolveCustomerLabel()  : string|null
resolveDefaultVatPercents()  : string|null
Resolves the effective default VAT rate, the placeholder of the `vatPercents` field in the work item grid.
resolveLoginErrorMessage()  : string
Resolves the flash message of a failed login.
resultDetail()  : string
Returns the failure detail for `%message%` in flash translations.
revokeTokensOnLogout()  : void
Calls the Api logout (revokes the refresh family and deny-lists the access `jti`) for the tokens of the current session. Fail-open on purpose: any failure is only logged, because the local logout must not stay unfinished.
saveWorkListForm()  : bool
Saves the form data; the redirect is handled by {@see \App\Core\Base\Admin\BasePresenter::createForm()}.
serviceLabel()  : string
Returns the service name in the current language (falling back to the first non-empty one, else `#id`).
setBillingFieldsFromCustomer()  : void
Overwrites invoice type and currency with the values from the newly chosen customer's billing profile.
setItemsCustomerContact()  : void
Refills the contact select options for the customer and resets the selected value.
setNetteUser()  : void
setWorkListFormDefaults()  : void
sheetErrorLine()  : string
sheetInvoiceNumbers()  : array<int, string>
Returns `workSheetId => invoice number` for invoiced sheets.
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.
storeEditorUpload()  : bool
Stores a file; the target folder may not exist (first upload into the root), so after a failure the root is created once and the write is retried. Deeper subfolders are created by the editor user through `editorCreateFolder`.
suggestCustomers()  : array<int, array{id: int, label: string, data: array}>
summarizeItemsRaw()  : array{count: int, hours: float, price: float}
Summarizes work items: count, hours and price.
tr()  : NotTranslate
Translates a key in the form domain to a ready string for fields with `setTranslator(null)`.
translatedName()  : string
Returns the entity name (with `translations`) in the current language, falling back to the first non-empty one.
tryRefresh()  : bool
Tries an auto-refresh for an expired access token. If the session holds a refresh token and the Api accepts it, stores the new pair in the session and returns true; otherwise returns false and the caller cancels the session.
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

Login page parameter with the key of the stored request to return to ({@see loginReturnArgs()}).

public string LOGIN_BACKLINK = 'backlink'

DEFAULT_ITEMS_PER_PAGE

Items per page of admin lists when the DB setting is missing.

protected mixed DEFAULT_ITEMS_PER_PAGE = 20

FORM_BACK_SUBSECTION

Session subsection mapping a key to the previous page URL of edit forms.

protected mixed FORM_BACK_SUBSECTION = 'formBack'

SESSION_NAMESPACE

protected mixed SESSION_NAMESPACE = 'admin'

BLOCKED_EXTENSIONS

Extensions that must not be uploaded into the web-served folder; otherwise an editor upload would mean running foreign code on the server. It is no content whitelist (the editor uploads what is needed), only a safeguard against executable files.

private mixed BLOCKED_EXTENSIONS = ['php', 'php3', 'php4', 'php5', 'php7', 'php8', 'phps', 'phtml', 'phar', 'cgi', 'pl', 'py', 'sh', 'bash', 'htaccess', 'htpasswd']

EDITOR_PAGE_LIMIT

Listing cap; the dialog is a suggester, not a paged list.

private mixed EDITOR_PAGE_LIMIT = 500

EDITOR_PAGE_TARGETS

Routes the editor may link to: text pages only.

private mixed EDITOR_PAGE_TARGETS = ['front:system:page:show']

Other page routes need parameters the editor cannot know (Front:System:User:profile wants id and username, Front:System:Registration:registration wants email and activationKey, advert details want id/name). A link to them could not be built or would lead to a broken address, so they stay out of the offer.

Another target (e.g. Front:System:Page:homepage or Front:System:Page:form) is one more line here, provided its route needs only slug.

EDITOR_UPLOAD_FOLDER

Editor root (relative to the `uploads/` root). Changing it to `''` would open the whole `uploads/` tree to the manager; the editor user could then reach (and delete) files of other modules such as products and avatars.

private mixed EDITOR_UPLOAD_FOLDER = 'editorUploads'

IMAGE_EXTENSIONS

Extensions the file manager shows as a preview instead of a document icon.

private mixed IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'svg', 'bmp']

LOCALE_MAP

Maps the short language code to the full locale of the registered NEON sources.

private mixed LOCALE_MAP = ['cs' => 'cs_CZ', 'en' => 'en_US', 'sk' => 'sk_SK']

The short code (cs, en, sk) comes from LanguageContext, derived from the URL parameter loc/lang; the full locales are cs_CZ, en_US, sk_SK. translator.neon configures one shared fallback chain [cs_CZ, cs, en_US, en, sk_SK, sk] for all languages, so a short code would always map to cs_CZ first, whatever language is active. A direct match on the full locale bypasses that.

The Contributte LocaleResolvers\Router (the only configured resolver) looks for the route parameter locale, but this application routes under loc, so it always returns null and the translator falls back to default: cs. An explicit setLocale() from here bypasses the resolver, which Translator::getLocale() asks only while no locale has been set explicitly.

Properties

$accessTokenValidator

Signature and expiry alone are not enough: a revoked token (`jti` on the deny-list) that has not expired yet would keep the session logged in although the Api guard rejects the same token. Same contract as {@see \App\Core\Traits\Api\Presenters\JwtAuthTrait::checkJwtAuth()}, one implementation.

public AccessTokenValidator $accessTokenValidator
Attributes
#[Inject]

$authUserManager

Admin `UserManager` for {@see UserManager::login()}. A module must not read through another module, so admin has its own `login()` that calls the same Api endpoint (`api/system/auth/login`).

public UserManager $authUserManager

The own property name avoids a collision with the generic $userManager from UserTrait (typed to the admin CRUD manager).

Attributes
#[Inject]

$columnSchema

Limits of the real database columns for {@see guardColumns()}.

public ColumnSchema $columnSchema
Attributes
#[Inject]

$editorAutosaveScope

Scope of the editor autosave drafts, see {@see \App\Core\Security\EditorAutosaveScope}.

public EditorAutosaveScope $editorAutosaveScope
Attributes
#[Inject]

$editorLinkPageManager

NOTE: own property name. `$pageManager` already exists on presenters (`PagePickerTrait`), and two `#[Inject]` properties of the same name would collide in the trait composition.

public PageManager $editorLinkPageManager
Attributes
#[Inject]

$editorUploadManager

NOTE: own property name, not `$fileSystemManager`.

public FileSystemManager $editorUploadManager

FileSystemPresenter takes the same manager as private readonly in its constructor, and PHP cannot redeclare a property of the same name between public and private.

Attributes
#[Inject]

$formSubmissionGuard

One-time submission tokens of "add" forms, see {@see guardFormSubmitOnce()}.

public FormSubmissionGuard $formSubmissionGuard
Attributes
#[Inject]

$loginDeviceCookie

Cookie of a known device for the password login throttle ({@see self::rememberLoginDevice()}).

public LoginDeviceCookie $loginDeviceCookie
Attributes
#[Inject]

$modalWindowFactory

Injected through Nette presenter inject (after construction), not the constructor.

public ModalWindowFactory $modalWindowFactory
Attributes
#[Inject]

$picker

Picker mode of a list; when not empty, the presenter renders its `show` grid as a selection modal (no add/edit/delete/checkboxes, each row has a select button) in the minimal `@picker.latte` layout.

public string $picker = ''

Opened by Picker; the value is the name of the opener's target field that receives the selection. Persistent, so it stays in grid links (filter/sort/paging) inside the modal. The presenter activates it in createComponent…Grid(): if ($this->picker !== '') { $grid->setPickerMode($this->picker); }.

Attributes
#[Persistent]

$pickerExclude

Id of the record that cannot be selected in picker mode (its select button is hidden).

public int $pickerExclude = 0

Serves self-referencing pickers (a parent page cannot be itself). Passed by Picker in the modal link via PickerConfig::targetParams; 0 means no exclusion.

Attributes
#[Persistent]

$pickerExcludeSubtreeLft

`lft`/`rgt` bounds of a whole subtree whose nodes cannot be selected in picker mode.

public int $pickerExcludeSubtreeLft = 0

Excludes the root and all descendants, so a node cannot be moved under its own descendant. 0 means no subtree exclusion.

Attributes
#[Persistent]

$pickerExcludeSubtreeRgt

public int $pickerExcludeSubtreeRgt = 0
Attributes
#[Persistent]

$queryCacheContext

Query cache policy for the whole admin request; `startup()` disables it.

public QueryCacheContext $queryCacheContext

Injected as a public property like the other admin base services; it cannot live in a trait because startup() touches it first.

Attributes
#[Inject]

$savedEntityId

Id of the record just saved; set by the `$processData` callback in `createForm()` after a successful save.

protected int|null $savedEntityId = null

The apply button (update) uses it: on the add action (new record, no id in the URL) it redirects to editing the new record (edit?id=...). Redirecting to this would lead back to an empty add form and the next apply would create a duplicate. When the callback leaves it null (e.g. a settings form without an edit action), the redirect goes to this.

$translatorDomain

protected string $translatorDomain = 'admin.company.presenters.workList'

$customerIdsWithAccount

Memoized set of customer ids that have a client account; see {@see customerIdsWithAccount()}.

private array<int, true>|null $customerIdsWithAccount = null

A map rather than a list so the grid can use isset() instead of in_array().

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

$invoicePreviewFailed

True when the preview failed to load; the modal then shows only the error, never a table.

private bool $invoicePreviewFailed = false

$invoicePreviewSheetIds

private array<int, int> $invoicePreviewSheetIds = []

selected work sheets, kept to re-render the form's hidden fields

$invoicePreviewSheets

private array<int, array<string, mixed>> $invoicePreviewSheets = []

work sheets from the preview response

$invoicePreviewTotals

private array{items: int, billableItems: int, skippedItems: int, hours: string, total: string} $invoicePreviewTotals = ['items' => 0, 'billableItems' => 0, 'skippedItems' => 0, 'hours' => '0.00', 'total' => '0.00']

$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()

$userPrivileges

private array<int, string> $userPrivileges = []

privileges of the current user loaded in startup()

$workItemAggregates

Memoized map of work item aggregates per job (over all its sheets); see {@see loadWorkItemAggregates()}.

private array<int, array{count: int, hours: string, price: string}>|null $workItemAggregates = null

Methods

__construct()

public __construct(DataGridFactory $dataGridFactory, PickerFactory $pickerFactory, WorkListManager $workListManager, WorkItemManager $workItemManager, CompanyManager $companyManager, WorkSheetManager $workSheetManager, ServiceManager $serviceManager, WorkGridFactory $workGridFactory, WorkItemCommentManager $workItemCommentManager, InvoiceManager $invoiceManager, CurrencyManager $currencyManager, InvoiceTypeManager $invoiceTypeManager, CountryManager $countryManager) : mixed
Parameters
$dataGridFactory : DataGridFactory
$pickerFactory : PickerFactory
$workListManager : WorkListManager
$workItemManager : WorkItemManager
$companyManager : CompanyManager
$workSheetManager : WorkSheetManager
$serviceManager : ServiceManager
$workGridFactory : WorkGridFactory
$workItemCommentManager : WorkItemCommentManager
$invoiceManager : InvoiceManager
$currencyManager : CurrencyManager
$invoiceTypeManager : InvoiceTypeManager
$countryManager : CountryManager

createComponent()

public createComponent(string $name) : IComponent|null
Parameters
$name : string
Return values
IComponent|null

flashFormOwnErrors()

Flashes form-level errors (`$form->getOwnErrors()`) as `danger` messages.

public flashFormOwnErrors(Form $form) : void

Called from onError of createForm(); public for component forms (AdminComponentFormTrait). Element errors are not flashed; {inputError} renders them at the field.

Parameters
$form : Form

flashRejectedUploads()

Flashes one `warning` listing the files the Api rejected (type, dimensions, unreadable) after a successful save of a form with a dropzone ({@see UploadRejection}).

public flashRejectedUploads(Result $result) : void

The save itself passed and accepted files are stored. The message is already translated (a list of names), so translate = false (see flashFormOwnErrors()). Call it before the redirect, because a flash sent through redirectUrl is lost; public for component forms.

Parameters
$result : Result

formatLayoutTemplateFiles()

Admin version of {@see \App\Core\Traits\Front\Presenters\ThemeInitTrait::formatLayoutTemplateFiles()}.

public formatLayoutTemplateFiles() : array<string|int, mixed>

It starts from AdminPage ($adminPageEntity, column layout), not from the Front $pageEntity, which an admin presenter does not have; admin also has no loggedLayout. The database value wins; getAdminLayout() is the fallback that LoginPresenter overrides with @login.latte.

Return values
array<string|int, mixed>

getContainer()

public getContainer() : Container
Return values
Container

getFormBackUrl()

Returns the stored previous page URL for the key, used by the `cancel` link in the template.

public getFormBackUrl(string|null $key) : string
Parameters
$key : string|null
Return values
string

guardFormSubmitOnce()

Protects a form against a double submit: one rendered form saves at most once.

public guardFormSubmitOnce(Form $form) : void

createForm() applies it to every form of an "add" action (isAddAction()), the component skeleton AdminComponentFormTrait likewise, and forms built outside both call it themselves (product import upload, credit notes, stocktake). A repeated submit of an edit form only saves the same values again, so edits are not guarded.

Adds the hidden token (FormSubmissionGuard::FIELD), issued on every GET render (on POST it comes back from the browser), and data-submit-once for submit-once.js, which blocks a second click in the browser. The server side (runSubmitOnce()) is the authority, because a request can come from outside the browser or the client script may not run. Idempotent.

Parameters
$form : Form

handleAddSheet()

Creates a sheet immediately so the grid has a `sheetId` for its row name prefix.

public handleAddSheet() : void
Attributes
#[Requires]
$methods: 'POST'

handleArchive()

Archives the job (`open` to `archived`); ends the job, it is not an invoicing action.

public handleArchive() : void
Attributes
#[Requires]
$methods: 'POST'

handleChangeLocale()

public handleChangeLocale(string $locale) : void
Parameters
$locale : string

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'

handleEditorCreateFolder()

Creates a subfolder in `folder` (path relative to the editor root).

public handleEditorCreateFolder([string $folder = '' ][, string $name = '' ]) : void
Parameters
$folder : string = ''
$name : string = ''
Attributes
#[Requires]
$methods: 'POST'

handleEditorDelete()

Deletes a file or (recursively) a folder. An empty path would point to the editor root, which cannot be deleted.

public handleEditorDelete([string $path = '' ]) : void
Parameters
$path : string = ''
Attributes
#[Requires]
$methods: 'POST'

handleEditorFiles()

Sends the folder listing for the editor file manager. The `folder` parameter is a path relative to the editor root (empty means the root).

public handleEditorFiles([string $folder = '' ]) : void

Sends JSON {folder, parent, breadcrumb[], folders[], files[]}; paths in the response are again relative to the editor root, and a public URL is given for files only. The server path never leaves the server.

A non-existent folder gives an empty listing, not an error (until something is uploaded the root does not exist physically and the manager just shows the empty state).

Parameters
$folder : string = ''

handleEditorPages()

Sends the page list for the link dialog as JSON `{"pages": [{id, label, url}, ...]}` sorted by label.

public handleEditorPages() : void

Only text pages (EDITOR_PAGE_TARGETS); pages without a filled URL (href) are skipped too, because no link can be built for them.

handleEditorRename()

Renames a file or folder (`path` relative to the editor root).

public handleEditorRename([string $path = '' ][, string $name = '' ]) : void
Parameters
$path : string = ''
$name : string = ''
Attributes
#[Requires]
$methods: 'POST'

handleEditorUpload()

Accepts a file from the editor (field `file`, multipart POST; optional `folder` = subfolder of the editor root) and sends JSON `{"location": "/www/uploads/editorUploads/..."}`, the format TinyMCE expects from `images_upload_handler`.

public handleEditorUpload() : void
Attributes
#[Requires]
$methods: 'POST'

handleInvoicePreview()

Opens the modal with a read-only invoice preview (AJAX).

public handleInvoicePreview([array<int, int|string> $sheetIds = [] ]) : void

An empty selection is treated as an error (no work sheets to invoice) without calling the bridge.

Parameters
$sheetIds : array<int, int|string> = []

handleItemComments()

Opens the modal with comments of one work item (AJAX).

public handleItemComments(int $itemId) : void
Parameters
$itemId : int

handleLogout()

public handleLogout() : void
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'

handleSignOut()

public handleSignOut() : void
Attributes
#[Requires]
$methods: 'POST'

handleUnarchiveModal()

Opens the "restore from archive" modal with a reason field (AJAX).

public handleUnarchiveModal() : void

hasServiceByType()

Returns whether the container has a service of the given type.

public hasServiceByType(string $className) : bool

The name is composed dynamically (module plus component), so class_exists() runs first, and a true result guarantees a valid type.

Parameters
$className : string
Tags
phpstan-assert-if-true

class-string $className

Return values
bool

initializeTranslator()

public initializeTranslator(string|null $alias) : void
Parameters
$alias : string|null

isAddAction()

Tells whether the current action creates a record: `add` and the `add<Something>` actions (`addMenu`, `addKey`). Derived from the server context only, like {@see formSubmitPrivilege()}.

public isAddAction() : bool
Return values
bool

isAllowed()

Returns whether the logged-in user has the given privilege.

public isAllowed(string $key) : bool

Reads the array loaded in startup(), with no database query. Fail-closed: returns false when loadUserPrivileges() was not called or failed.

Public so templates can call it ($presenter->isAllowed(...)) for conditional display of actions outside DataGrid (e.g. static menu management links).

Parameters
$key : string
Return values
bool

isSubmitOnceForm()

Tells whether the form was protected by {@see guardFormSubmitOnce()}.

public isSubmitOnceForm(Form $form) : bool
Parameters
$form : Form
Return values
bool

loginWithCredentials()

Logs in with username and password; called only by the login form (after validation and CSRF).

public loginWithCredentials(string $username, string $password[, bool $remember = false ]) : void

IMPORTANT: this must not be named handleLogin(). A handle* method is a public signal of every presenter (?do=login&username=...&password=...): the password could end up in the URL and the server log, and the form token would be bypassed. Nette finds signals by the handle prefix only.

Parameters
$username : string
$password : string
$remember : bool = false

publishFieldErrors()

Publishes element errors of a failed submit to the client.

public publishFieldErrors(Form $form) : void

IMPORTANT: field errors were visible only where the template has {inputError}; server rules on fields without it stopped the form but showed the user no message.

An AJAX submit sends payload.fieldErrors; a full render sets data-server-field-errors on <body> (see @layout.latte), which the script reads on load. The script avoids duplicating {inputError}.

Parameters
$form : Form

redirectToFormBack()

Redirects to the stored previous page and discards the key.

public redirectToFormBack(string|null $key) : void

Public for component forms (AdminComponentFormTrait), whose save submit calls it through $this->getPresenter().

Parameters
$key : string|null

renderShow()

`show` is the default action of admin presenters (route fallback).

public renderShow() : void

resolveFormBackKey()

Returns the previous-page key: from the URL if present (survives the update redirect), otherwise on a plain GET render of the edit page stores `Referer` in the session under a new random key.

public resolveFormBackKey() : string

Public for component forms (AdminComponentFormTrait); a component reaches it through $this->getPresenter().

Return values
string

runSubmitOnce()

Runs a saving step of a form under its one-time token (a form without the token just runs it).

public runSubmitOnce(Form $form, string $backKey, callable(): bool $save) : bool|null
  • the first submit claims the token and saves; a failed save (false or an exception) releases it, so the corrected form can be sent again; a successful one keeps it with $savedEntityId. A redirect thrown from inside the step counts as a successful save, so the step should set $savedEntityId before redirecting;
  • a submit of an already used token does not run the step, flashes a notice, sets $savedEntityId to the record of the first submit and returns null; the caller leads where the first submit led;
  • a submit arriving while the first one still saves waits for it a few seconds, then behaves as above, or ends on the previous page with a warning (redirect);
  • a missing or malformed token does not run the step; the form gets a fresh token and a message and false is returned (the browser case is a form opened before the protection was deployed).

Public for component forms and form traits outside createForm().

Parameters
$form : Form
$backKey : string
$save : callable(): bool
Return values
bool|null —

true = saved now, false = not saved, null = saved by an earlier submit of the same form

setConfigParameters()

public setConfigParameters([bool $debugMode = false ][, bool $productionMode = false ]) : void
Parameters
$debugMode : bool = false
$productionMode : bool = false

setTranslator()

public setTranslator(ITranslator $translator) : $this
Parameters
$translator : ITranslator
Return values
$this

typedComponent()

Returns a subcomponent with a verified type.

public typedComponent(string $name, T> $type) : T

Use it instead of an inline @var above $this['x'] (which does not narrow the type) and instead of the generic IComponent that $this['x'] returns. See Form::typedComponent().

Parameters
$name : string
$type : T>
Tags
template
Return values
T

authorize()

Authorization hook; by default requires the `:Admin` privilege.

protected authorize() : void

LoginPresenter overrides it so the login page stays reachable for a guest.

authorizeFormSubmit()

Gate for writing presenter forms built by `createForm()`.

protected authorizeFormSubmit(Form $form) : void

IMPORTANT: a form submit is a signal (?do=editForm-submit) that Nette handles on any action, after action*(). A gate in actionAdd()/actionEdit() therefore covers only submits sent to add/edit; a submit sent to a list (show) passes authorizeListing() only, and a save callback without its own gate would write with view-only rights.

Called by createForm() before $processData (only for the update/save buttons) and by custom saving submits outside that gate.

Parameters
$form : Form

authorizeListing()

Gate for reading lists.

protected authorizeListing() : void

IMPORTANT: without this gate anyone inside the admin could open any list without the privilege. The gate lives here once, not in every presenter. The key is derived as in PrivilegeCheckTrait::checkCurrentPrivilege(): :Admin:<Module>:<Presenter>:show.

IMPORTANT: guards only the show action. Other actions have their own gates (add/edit/signals), and a blanket key would not fit them: dashboard, login, downloadPdf, statistics and similar have no ...:<action> key, so the fail-closed check would lock out even the administrator.

NOTE: every show screen needs its key in the privilege table and enabled for the admin role.

checkAdminAccess()

Entry gate to the admin: a guest is sent to login; a logged-in user without `:Admin` is sent to login with an error.

protected checkAdminAccess() : void

checkCurrentPrivilege()

Builds the key from the current request (`:Admin:<Module>:<Presenter>:<action>`) and checks it. `getName()` already contains the `Admin:...` prefix, so only one leading colon is added.

protected checkCurrentPrivilege() : void

Use it where the key matches the action; use explicit checkPrivilege() where it differs (another presenter, add versus edit).

checkPrivilege()

Checks a specific privilege; throws 403 when denied.

protected checkPrivilege(string $key) : void

IMPORTANT: a hidden button is no protection. A hand-built ?...&do=<signal> URL runs the action, so every non-standard action and every mutating signal needs the gate as its first statement; otherwise anyone who types the URL can run it.

IMPORTANT: form actions need two gates, one in the signal and one in process*() (the submit), because a submit arrives as its own request. With only the signal gate the submit stays open. See the WorkList traits.

Parameters
$key : string

createComponentPicker()

protected createComponentPicker() : Multiplier<string|int, Picker>
Return values
Multiplier<string|int, Picker>

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>

createForm()

Creates an edit form with the standard actions and a return to the previous page.

protected createForm(callable(ArrayHash<string|int, mixed>, Form): bool $processData) : Form

Buttons (the template renders them on the right as cancel, update, save):

  • save saves and returns to the previous page,
  • update saves and stays on the edit page (redirect to this),
  • cancel is only a link back, rendered by the template via getFormBackUrl().

On the first load of the edit page (plain GET render, not POST/signal) the previous page is taken from Referer, stored in the session under a random key, and the key goes into the hidden backKey field. It is per form so concurrent tabs do not overwrite each other, and it travels through the update redirect as a URL parameter.

The concrete presenter adds fields and defaults; this method provides the skeleton, buttons and redirects.

Parameters
$processData : callable(ArrayHash<string|int, mixed>, Form): bool

Saves the data; returns success (handles its own flash).

Return values
Form

currentLangId()

Returns the current language id (for picker labels etc.), null when unknown.

protected currentLangId() : int|null
Return values
int|null

customerDeliveryPicker()

protected customerDeliveryPicker(string $formName, string $targetField[, string|null $submitButton = null ]) : PickerConfig
Parameters
$formName : string
$targetField : string
$submitButton : string|null = null
Return values
PickerConfig

customerPicker()

protected customerPicker(string $formName, string $targetField[, string|null $submitButton = null ]) : PickerConfig
Parameters
$formName : string
$targetField : string
$submitButton : string|null = null
Return values
PickerConfig

domainTranslator()

Returns a translator prefixed with the presenter domain, or `null` (empty domain or a translator other than Contributte).

protected domainTranslator() : PrefixedTranslator|null

beforeRender() sets it only on $this->template. Templates created separately through createTemplate() (e.g. modal content built in the handle phase) do not have it, and their relative {_'...'} keys would print raw. Such places obtain it here.

Return values
PrefixedTranslator|null

formSubmitPrivilege()

Returns the privilege key required to save a form built by `createForm()`.

protected formSubmitPrivilege(Form $form) : string

Derived only from server context (presenter and action), never from a form field, so a changed hidden id cannot select a different right:

  • action add gives ...:add, action edit gives ...:edit;
  • any other action of a presenter with records (it has an add/edit action) gives ...:edit; a record form does not belong there, and whoever sends it needs at least the edit right;
  • a presenter without add/edit actions (single-page settings) gives the key of the current action, i.e. ...:show. Those screens have no other key in RBAC; an unknown action ends on a nonexistent key, which is a 403 (fail-closed).

A form bound to a custom action (sendEmail, inventory, addMenu...) overrides the method in its presenter and returns that action's key, always matched by form name so the exception does not let another form of the same presenter through.

Parameters
$form : Form
Return values
string

getAdminItemsPerPage()

Returns the default items per page of admin lists from the DB setting `admin.itemsPerPage` (module System = 1), falling back to {@see DEFAULT_ITEMS_PER_PAGE}.

protected getAdminItemsPerPage() : int
Return values
int

getAdminLayout()

Returns the layout name; in picker mode ({@see $picker}) the `show` grid uses the minimal `@picker.latte` (no sidebar/header) so the opener fetch returns only the table.

protected getAdminLayout() : string

LoginPresenter overrides it with @login.latte.

Return values
string

getFormBackFallback()

Returns the return URL when the previous page is unknown; override in a presenter without a `show` action.

protected getFormBackFallback() : string
Return values
string

getFormLanguages()

Returns the languages for multilingual form fields (admin `LanguageManager`).

protected getFormLanguages() : array<int, Language>
Return values
array<int, Language>

getSessionKey()

Returns a session key string, e.g. 'front.auth.step1'.

protected getSessionKey([string|null $namespace = null ][, string|null $subsection = null ], string ...$extra) : string
Parameters
$namespace : string|null = null

Base namespace (module prefix). Uses SESSION_NAMESPACE if null.

$subsection : string|null = null

Subsection under the namespace. Uses SESSION_SUBSECTION constant if null.

$extra : string

Additional parts to append (e.g. 'step1', 'wizard', etc.).

Return values
string

getSessionSection()

Returns a Nette\SessionSection instance using a dynamically built session key.

protected getSessionSection([string|null $namespace = null ][, string|null $subsection = null ], string ...$extra) : SessionSection
Parameters
$namespace : string|null = null
$subsection : string|null = null
$extra : string
Tags
see
getSessionKey()
Return values
SessionSection

guardColumns()

Gates every text and number field of the form by the real column it writes to ({@see \App\Core\Forms\ColumnGate::guard()}): an oversized value is an error at the field, never a database error that closes the entity manager (admin walkthrough A1, A14).

protected guardColumns(Form $form, array<string, array<int, class-string>> $containers[, array<int, string> $except = [] ]) : void
Parameters
$form : Form
$containers : array<string, array<int, class-string>>

container path ('' = the form, a multiplier name = its rows) => entities

$except : array<int, string> = []

control names never gated (a value transformed before storage)

intOrNull()

Converts mixed to int or null (an empty or zero value becomes null).

protected intOrNull(mixed $value) : int|null
Parameters
$value : mixed
Return values
int|null

isSameSiteRequest()

Tells whether the request comes from the own site (Nette SameSite cookie); protects GET actions with a side effect.

protected isSameSiteRequest() : bool

getHttpRequest() returns the IRequest interface, while isSameSite() exists only on Nette\Http\Request.

Return values
bool

isSupplyPlaceRelevant()

Tells whether the job involves work on immovable property (VAT place of supply).

protected isSupplyPlaceRelevant() : bool

True when any work item has a supply kind with nature immovable_service or, for a job without items, when the company's default supply kind has it (that is what such work is eventually invoiced with).

Only a display condition of the "supply place country" field; the value is always saved, so switching the supply kind back restores access without loss.

Return values
bool

loadUserPrivileges()

Loads all privileges of the logged-in user with one database query. Call it right after `checkAdminAccess()` in `startup()` (the user is guaranteed to be logged in by then).

protected loadUserPrivileges() : void

parseDateTime()

Converts a `datetime-local` input or text value to `DateTime`, or null when empty or unparsable.

protected parseDateTime(mixed $value) : DateTime|null
Parameters
$value : mixed
Return values
DateTime|null

pickers()

Returns the presenter's pickers: the customer (Invoicer `Customer`) with its own `PickerConfig` and a refresh submit button.

protected pickers() : array<string, PickerConfig>

It deliberately does not use customerPicker() from the trait: its fieldMap targets the invoicing fields, while here only the contact is wanted. Pure and idempotent; also called on the AJAX signal suggest!.

Return values
array<string, PickerConfig>

setupTemplateTranslator()

Sets a translator prefixed with the presenter domain into the template (as Front components do, see {@see \App\Core\Base\BaseComponent::setup()}).

protected setupTemplateTranslator() : void

Templates then use short keys {_'key'} resolved as <translatorDomain>.key without {translator}. Shared keys in the layout are written absolutely with the // prefix, which bypasses the domain. The domain comes from the $translatorDomain property of the concrete presenter (admin.system.presenters.<presenter>).

str()

Safely converts a mixed form value to string.

protected str(mixed $value) : string
Parameters
$value : mixed
Return values
string

strOrNull()

Converts mixed to string or null (an empty string is stored as null).

protected strOrNull(mixed $value) : string|null
Parameters
$value : mixed
Return values
string|null

addBasicFields()

Adds company, name and internal note.

private addBasicFields(Form $form) : void
Parameters
$form : Form

addBillingFields()

Adds the "Invoicing" section: invoice type and currency directly on the job, both optional.

private addBillingFields(Form $form) : void

An empty value is a signal for the next link of the chain (customer, company; CompanyInvoiceGenerator), hence setNullable(), not setRequired(). checkDefaultValue(false) is needed because the refresh handler changes the options and Nette would otherwise silently reject a default outside the current list.

Parameters
$form : Form

addClientFields()

Adds the customer (picker, hidden `invoicerCustomerId`) and the customer contact (select).

private addClientFields(Form $form) : void

The control lives in the form and the Picker component only renders it via {control picker-invoicerCustomerId}, so the template has no separate {input invoicerCustomerId}. Contact options are filled by the refresh handler and the edit defaults.

Without a customer the job cannot be invoiced (internal work uses the company's own customer). This is enforced at invoicing (WorkListManager::invoiceSheets() and the preview, code customerMissing), not on save, so older jobs without a customer stay editable.

IMPORTANT: if setRequired() is ever added here, note that HiddenField::addError() always redirects the error to the form level, so {inputError invoicerCustomerId} prints nothing; the message is shown only by BasePresenter::flashFormOwnErrors().

Parameters
$form : Form

addPickerRefreshButtons()

Adds the hidden `customerRefresh` submit (`validationScope([])`, class `ajax`, `data-picker-refresh`), which `picker.js` clicks after a selection.

private addPickerRefreshButtons(Form $form) : void
Parameters
$form : Form

applyCustomerRefresh()

Handles the customer refresh.

private applyCustomerRefresh(Form $form) : void

Loads the contacts of the newly chosen customer into invoicerCustomerContactId and resets its value (the old contact may not belong to the new customer), prefills invoice type and currency from the customer's billing profile (setBillingFieldsFromCustomer()) and redraws the workListForm snippet. It does not touch any client account, which lives on the customer.

Parameters
$form : Form

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}

buildContactOptions()

Builds customer contact options as `id => "name2/street, city (email)"` (only the parts the contact has).

private buildContactOptions(Customer|null $customer) : array<int, string>
Parameters
$customer : Customer|null
Return values
array<int, string>

buildWorkListEntity()

Builds the job entity from form values.

private buildWorkListEntity(ArrayHash<string|int, mixed> $values) : WorkList

IMPORTANT: state and closedDatetime are never set here; they are #[ExternalReadonly] plus defaultSkipFields and change only through the archive actions.

Parameters
$values : ArrayHash<string|int, mixed>
Return values
WorkList

createIdentity()

private createIdentity(array<string, mixed> $payload, string $token) : SimpleIdentity|null
Parameters
$payload : array<string, mixed>
$token : string
Return values
SimpleIdentity|null

createMultiplierComponent()

Creates a multiplier for the component.

private createMultiplierComponent(string $name) : Multiplier<string|int, IComponent>
Parameters
$name : string
Return values
Multiplier<string|int, IComponent>

createNewComponentInstance()

Creates a new component instance without adding it to the presenter; used by the multiplier.

private createNewComponentInstance(string $baseName[, int|string|null $uniqueKey = null ]) : IComponent|null
Parameters
$baseName : string
$uniqueKey : int|string|null = null
Return values
IComponent|null

createStandardComponent()

Creates a standard component (not a multiplier).

private createStandardComponent(string $name) : IComponent|null
Parameters
$name : string
Return values
IComponent|null

customerDeliveryFieldMap()

private customerDeliveryFieldMap() : array<string, string>
Return values
array<string, string> —

mapping to the delivery fields (customerDlv*)

customerFieldMap()

Returns the mapping data key (from {@see customerPrefillData()}) to billing field. Shared by the picker (client `fieldMap`) and the server-side prefill (snippet redraw after a pick).

private customerFieldMap() : array<string, string>
Return values
array<string, string>

customerIdsWithAccount()

Returns ids of customers that have a client account (`Customer.userId`).

private customerIdsWithAccount() : array<int, true>

The account lives on the customer, not on the job. One query instead of one per grid row (the customer table is small); the filter is applied in PHP, like in loadWorkItemAggregates(). Lazily memoized; the first call comes from the ComputedRenderer callback of the account column.

Return values
array<int, true>

editorBreadcrumb()

Builds the breadcrumb navigation: cumulative segment paths.

private editorBreadcrumb(string $relative) : array<int, array{label: string, path: string}>
Parameters
$relative : string
Return values
array<int, array{label: string, path: string}>

editorLinkLangId()

Returns the language used to pick labels and URLs (default 1, Czech).

private editorLinkLangId() : int
Return values
int

editorPageLabel()

Returns the page label: title, otherwise name, otherwise `#id`.

private editorPageLabel(Page $page, int $langId) : string
Parameters
$page : Page
$langId : int
Return values
string

editorPageRoute()

Returns the first page route the editor may link to. A page may have several (many-to-many); the whitelisted one is taken, not the first in order, so a page with both `Page:show` and `Page:form` ends up on the right one.

private editorPageRoute(Page $page) : Route|null
Parameters
$page : Page
Return values
Route|null

editorPageUrl()

Returns the front end address of a page. The router builds it from the page route, so `/o-nas` and any prefix come out on their own.

private editorPageUrl(Page $page, int $langId) : string|null
Parameters
$page : Page
$langId : int
Return values
string|null —

null when the page is not a text page (see EDITOR_PAGE_TARGETS), has no filled URL, or its route cannot build a link

editorParentFolder()

Returns the parent folder (null in the editor root).

private editorParentFolder(string $relative) : string|null
Parameters
$relative : string
Return values
string|null

editorPath()

Converts a client path (relative to the editor root) to an Api path (relative to the `uploads/` root).

private editorPath(string $relative) : string|null

This is the only place where the prefix is added, so the segment check lives here too. The Api rejects .. as well, but without this check a ../ from the request would not escape uploads/ yet would escape editorUploads/ into neighboring folders of other modules.

Parameters
$relative : string
Return values
string|null —

null when the path contains a forbidden segment

editorUploadName()

Builds a safe and unique file name. Sanitization copies the Api layer rules (webalize of the base and of the extension), so the name returned in the URL is exactly the name the file is stored under. The random suffix prevents collisions: `writeFile()` overwrites the same name without asking, so two different `obrazek.jpg` files would overwrite each other.

private editorUploadName(string $untrustedName) : string|null
Parameters
$untrustedName : string
Return values
string|null —

null for a forbidden extension or an unusable name

editorUploadUrl()

Returns the public URL of a file. `$path` is relative to the `uploads/` root (so it includes `editorUploads/`), the same shape as other uploads in the system.

private editorUploadUrl(string $path) : string
Parameters
$path : string
Return values
string

ensureEditorRoot()

Creates the editor root folder; when it already exists the Api returns false and nothing happens.

private ensureEditorRoot() : void

expireIdleLogin()

Returns whether the login expired from inactivity. If so, revokes the tokens (like a logout), clears the `auth` section, logs out the Nette identity and returns true; otherwise moves the activity stamp forward.

private expireIdleLogin(AuthSessionStorage $authStorage) : bool

Admin keeps 30 min, or 8 h with its own "remember me", regardless of the front end "remember me" (the auth section is shared, its 31 days do not count here). An expiry in admin logs out of the front end too (one session).

Parameters
$authStorage : AuthSessionStorage
Return values
bool

findCommentedItem()

Returns the work item by id, or `null` when it does not exist or is outside the edited job.

private findCommentedItem(int $itemId) : WorkItem|null
Parameters
$itemId : int
Return values
WorkItem|null

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

initializeMetaTags()

Fills the template with the title, meta tags and HTML text of the page from the translation (`PageText`) of the current language, for the layout (like the Front {@see \App\Core\Traits\Front\Presenters\PageInitTrait}).

private initializeMetaTags(AdminPage|null $adminPageEntity) : void

When the page or translation is missing, the values are empty strings and the layout uses its fallbacks.

Parameters
$adminPageEntity : AdminPage|null

isItemInWorkList()

Tells whether the work item belongs, via its work sheet, to the given job.

private isItemInWorkList(WorkItem $item, int $workListId) : bool
Parameters
$item : WorkItem
$workListId : int
Return values
bool

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

loadCompanyForSupplyPlace()

Loads the job's company for its default supply kind; `null` when the job has none yet.

private loadCompanyForSupplyPlace(int|null $companyId) : Company|null
Parameters
$companyId : int|null
Return values
Company|null

loadCustomer()

Loads the customer including contacts for the server-side prefill of the invoice snapshot.

private loadCustomer(int $id) : Customer|null
Parameters
$id : int
Return values
Customer|null

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>

loadWorkItemAggregates()

Aggregates work items per job (count, total hours, total price over all its sheets).

private loadWorkItemAggregates() : array<int, array{count: int, hours: string, price: string}>

One query loads non-deleted sheets (map sheetId => workListId), one loads non-deleted items, and the sums are done in PHP; volumes are small, so there is no SQL aggregate across the Admin/Api boundary. Lazily memoized into a property (first ComputedRenderer callback of the aggregation columns).

Return values
array<int, array{count: int, hours: string, price: string}> —

workListId => aggregate

localizationCurrencyKey()

Admin ignores the localization currency (it takes the language only).

private localizationCurrencyKey(object $localization) : string|null
Parameters
$localization : object
Return values
string|null

loginReturnArgs()

Remembers the request of a logged-out user so the login can return to it ({@see storeRequest()}, `backlink` of the login page, restored by `AuthStorageTrait::redirectAfterLoginAction()`).

private loginReturnArgs() : array<string, string>

Without it a save sent after a logout in another tab (or after the idle timeout) ended on the login page with no explanation, the login led to the dashboard and the filled-in form was lost. A restored POST renders the form again with the submitted values, but Nette does not process it (Request::RESTORED, no blind replay): the admin checks the values and saves once more. An AJAX request is not stored (naja would replay it as a page). The user id stored with the request is the previous identity, so a different account logging in is not sent there.

Return values
array<string, string>

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

parsePickerName()

Splits a picker instance name into the config key plus row indexes (multiplier and nested multiplier). `parentId` -> `['parentId', []]`; `address_2` -> `['address', ['2']]`; `parameter_1_0` -> `['parameter', ['1','0']]` (nested).

private parsePickerName(string $name) : array{0: string, 1: list}

The separator is an underscore, not a dash: - is the component path separator in Nette ({control picker-address_0} = Multiplier picker -> child address_0). A config key must not end with _<number> (keys: parentId, address, parameter).

Parameters
$name : string
Return values
array{0: string, 1: list}

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

prepareCompanyOptions()

Returns company options as `id => internal name` (`Company.name` is a flat column, untranslated).

private prepareCompanyOptions() : array<int, NotTranslate>

Shared by the grid (column) and the form (select).

Return values
array<int, NotTranslate>

prepareCurrencyOptions()

The name is already translated DB data, not a key, hence `NotTranslate`.

private prepareCurrencyOptions() : array<int, NotTranslate>
Return values
array<int, NotTranslate>

prepareCustomerOptions()

Returns customer options as `id => name` (via `$this->customerManager` injected by {@see CustomerPickerTrait}), only for the grid column.

private prepareCustomerOptions() : array<int, string>

A NULL customer (internal job) is handled separately, see createComponentGrid().

Return values
array<int, string>

prepareInvoiceTypeOptions()

private prepareInvoiceTypeOptions() : array<int, NotTranslate>
Return values
array<int, NotTranslate>

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

processComment()

Saves a comment. The server sets author and time; only the text comes from the form.

private processComment(ArrayHash<string|int, mixed> $values) : void
Parameters
$values : ArrayHash<string|int, mixed>

processInvoiceConfirm()

private processInvoiceConfirm(ArrayHash<string|int, mixed> $values) : void
Parameters
$values : ArrayHash<string|int, mixed>

processLogin()

private processLogin(string $username, string $password[, bool $remember = false ]) : void
Parameters
$username : string
$password : string
$remember : bool = false

processLoginException()

private processLoginException(Exception $e) : void
Parameters
$e : Exception

processUnarchive()

private processUnarchive(string|null $reason) : void
Parameters
$reason : string|null

readLoginDeviceToken()

Returns the raw known-device cookie for the Api login (the Api verifies its signature).

private readLoginDeviceToken() : string|null
Return values
string|null

redirectAfterLoginAction()

After a login attempt: a successful login returns to the request remembered before the login (`backlink`, {@see PrivilegeCheckTrait::loginReturnArgs()}), otherwise to the dashboard; a failed one stays on the login page and keeps the way back.

private redirectAfterLoginAction() : void

refreshNetteIdentity()

Refreshes the token of the same logged-in user: identity data (token, name, email) are overwritten in the stored object without `login()`, so neither the session id nor the form CSRF tokens change. The identity is stored in the session section as an object; the change is written at the end of the request.

private refreshNetteIdentity(array<string, mixed> $payload, string $token) : bool
Parameters
$payload : array<string, mixed>
$token : string
Return values
bool —

false when impossible (nobody logged in or another user); the caller uses login

rememberLoginDevice()

After a successful login (password or OAuth) issues or renews the known-device cookie ({@see LoginDeviceCookie}): a signed proof that this browser has already logged in to the account. The next login sends it to the Api ({@see self::processLogin()}), and above the account limit the password from this browser is still verified, so foreign attempts do not lock out the owner. HttpOnly, SameSite=Lax, Secure on HTTPS; lifetime from `ratelimit.loginDevice.cookieLifetime`.

private rememberLoginDevice(mixed $payload) : void
Parameters
$payload : mixed

resolveAdminAuthorName()

Returns the logged-in admin's display name; the fallback keeps `author_name` non-empty.

private resolveAdminAuthorName() : string
Return values
string

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

resolveCustomerLabel()

private resolveCustomerLabel(int $id) : string|null
Parameters
$id : int
Return values
string|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

resolveLoginErrorMessage()

Resolves the flash message of a failed login.

private resolveLoginErrorMessage(Result $result) : string

IMPORTANT: a brute-force throttle must not be shown as "wrong password"; the limit would then look like an inexplicably failing form.

results.rateLimitExceeded (the generic text of ResultCode) carries no seconds and does not belong to login. The login flow therefore has its own key in the same family as admin.general.login.error.message: login.rateLimit.message, with the seconds passed as %count% (Contributte: numeric 2nd argument).

The Api sends retryAfter in data (AuthPresenter::rateLimitedResult()); the internal bridge call does not see the Retry-After header.

Parameters
$result : Result
Return values
string

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

revokeTokensOnLogout()

Calls the Api logout (revokes the refresh family and deny-lists the access `jti`) for the tokens of the current session. Fail-open on purpose: any failure is only logged, because the local logout must not stay unfinished.

private revokeTokensOnLogout(AuthSessionStorage $authStorage, mixed $userId) : void
Parameters
$authStorage : AuthSessionStorage
$userId : mixed

saveWorkListForm()

Saves the form data; the redirect is handled by {@see \App\Core\Base\Admin\BasePresenter::createForm()}.

private saveWorkListForm(ArrayHash<string|int, mixed> $values) : bool

After the job is saved, the POSTed sheets field (work sheets and items) is processed too (WorkListSheetsFormTrait::persistSheetsAndItems()). Partial errors in individual rows or sheets do not fail the job: they are collected and flashed separately, and everything saved stays saved.

Parameters
$values : ArrayHash<string|int, mixed>
Return values
bool

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

setBillingFieldsFromCustomer()

Overwrites invoice type and currency with the values from the newly chosen customer's billing profile.

private setBillingFieldsFromCustomer(Form $form, Customer|null $customer) : void

Refresh is a deliberate customer choice, so even a different (e.g. proforma) value already on the job is replaced; the admin can override again afterwards. When the profile has no value, null is set and the prompt then means fallback to the company settings (chain in the generator).

Parameters
$form : Form
$customer : Customer|null

setItemsCustomerContact()

Refills the contact select options for the customer and resets the selected value.

private setItemsCustomerContact(Form $form, Customer|null $customer) : void
Parameters
$form : Form
$customer : Customer|null

setNetteUser()

private setNetteUser(array<string, mixed>|null $payload, string $token) : void
Parameters
$payload : array<string, mixed>|null
$token : string

sheetInvoiceNumbers()

Returns `workSheetId => invoice number` for invoiced sheets.

private sheetInvoiceNumbers(array<int, WorkSheet> $sheets) : array<int, string>

The chip on a sheet must carry the invoice number, not the internal invoicer_invoice_id, by which the admin cannot find the invoice in Invoicer. The front end gets the same via the transient invoiceNumber (WorkSheetManager::findInvoiceNumbers()); the admin reads with its own InvoiceManager.

One query with InFilter, no N+1. A missing or deleted invoice is just an omitted key, never an error: the template falls back to the id and the screen must not fail because of a number.

Parameters
$sheets : array<int, WorkSheet>
Return values
array<int, 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

storeEditorUpload()

Stores a file; the target folder may not exist (first upload into the root), so after a failure the root is created once and the write is retried. Deeper subfolders are created by the editor user through `editorCreateFolder`.

private storeEditorUpload(string $folder, string $name, string $base64) : bool
Parameters
$folder : string
$name : string
$base64 : string
Return values
bool

suggestCustomers()

private suggestCustomers(string $query) : array<int, array{id: int, label: string, data: array}>
Parameters
$query : string
Return values
array<int, array{id: int, label: string, data: array}>

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}

tr()

Translates a key in the form domain to a ready string for fields with `setTranslator(null)`.

private tr(string $key) : NotTranslate

IMPORTANT: Nette getLabel() and Validator::formatMessage() always use the form's translator, not the per-control override. Without NotTranslate the finished text would be translated again as a key and Tracy would report a missing translation.

Parameters
$key : string
Return values
NotTranslate

translatedName()

Returns the entity name (with `translations`) in the current language, falling back to the first non-empty one.

private translatedName(object $entity) : string
Parameters
$entity : object
Return values
string

tryRefresh()

Tries an auto-refresh for an expired access token. If the session holds a refresh token and the Api accepts it, stores the new pair in the session and returns true; otherwise returns false and the caller cancels the session.

private tryRefresh(AuthSessionStorage $authStorage) : bool
Parameters
$authStorage : AuthSessionStorage
Return values
bool

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