Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0601c8cb2 | ||
|
|
5b320bf2ea | ||
|
|
15110f78d8 | ||
|
|
4154de6bd1 | ||
|
|
8094fcf5b5 | ||
|
|
77afa207e0 | ||
|
|
d9ef6dfcad | ||
|
|
ff6918fcab | ||
|
|
f376b5c8a1 | ||
|
|
b00fbd2516 | ||
|
|
d97abc3f57 | ||
|
|
3f184e42c8 | ||
|
|
4a31411d69 | ||
|
|
3925eacf9f | ||
|
|
394f377890 | ||
|
|
e8b0dc4969 |
1
.github/workflows/docker.yaml
vendored
1
.github/workflows/docker.yaml
vendored
@@ -73,6 +73,7 @@ jobs:
|
||||
target: prod
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
kimai/kimai2:stable
|
||||
kimai/kimai2:apache
|
||||
kimai/kimai2:apache-${{ env.kimai_version }}
|
||||
push: true
|
||||
|
||||
13
README.md
13
README.md
@@ -6,11 +6,9 @@
|
||||
<a href="https://github.com/kimai/kimai/actions"><img alt="CI Status" src="https://github.com/kimai/kimai/actions/workflows/testing.yaml/badge.svg"></a>
|
||||
<a href="https://codecov.io/gh/kimai/kimai"><img alt="Code Coverage" src="https://codecov.io/gh/kimai/kimai/branch/main/graph/badge.svg"></a>
|
||||
<a href="https://packagist.org/packages/kimai/kimai"><img alt="Latest stable version" src="https://poser.pugx.org/kimai/kimai/v/stable"></a>
|
||||
<a href="https://www.gnu.org/licenses/agpl-3.0.en.html"><img alt="License" src="https://poser.pugx.org/kimai/kimai/license"></a>
|
||||
<a href="https://phpc.social/@kimai" rel="me"><img alt="Mastodon" src="https://img.shields.io/badge/toot-%40kimai-8c8dff"></a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">Kimai - time-tracker</h1>
|
||||
<h1 align="center">Kimai<br>#1 Open-Source Time-Tracker</h1>
|
||||
|
||||
Kimai is a professional grade time-tracking application, free and open-source.
|
||||
It handles use-cases of freelancers as well as companies with dozens or hundreds of users.
|
||||
@@ -30,7 +28,7 @@ and so much more.
|
||||
### Requirements
|
||||
|
||||
- PHP 8.1.3 minimum with support for 8.2, 8.3, 8.4, 8.5
|
||||
- MariaDB or MySQL
|
||||
- MariaDB / MySQL: oldest maintained LTS release (MariaDB >= [10.6](https://endoflife.date/mariadb) or MySQL >= [8.4](https://endoflife.date/mysql)) or newer
|
||||
- A webserver and subdomain (subdirectory is not supported)
|
||||
- PHP extensions: `gd`, `intl`, `json`, `mbstring`, `pdo`, `tokenizer`, `xml`, `xsl`, `zip`
|
||||
|
||||
@@ -71,6 +69,7 @@ The best way to start is to [open a new issue](https://github.com/kimai/kimai/is
|
||||
In case you want to contribute, but you wouldn't know how, here are some suggestions:
|
||||
|
||||
- Spread the word: Please [write a testimonial for our Wall of love](https://love.kimai.org), vote for Kimai on any software platform, you can toot or tweet about it, share it on LinkedIn, Reddit and any other social media platform!
|
||||
- [Translate Kimai into your language](https://hosted.weblate.org/engage/kimai/), or help to improve the existing translations, many languages look for a contributor
|
||||
- Answer questions: You know the answer to another user's problem? Share your knowledge.
|
||||
- Something can be done better? An essential feature is missing? Create a feature request.
|
||||
- Report bugs makes Kimai better for everyone.
|
||||
@@ -79,6 +78,12 @@ In case you want to contribute, but you wouldn't know how, here are some suggest
|
||||
|
||||
There is one simple rule in our "Code of conduct": Don't be an ass!
|
||||
|
||||
## Follow Kimai
|
||||
|
||||
- Mastodon: [@kimai](https://phpc.social/@kimai)
|
||||
- Youtube: [@kimai_org](https://www.youtube.com/@kimai_org)
|
||||
- LinkedIn: [@kimai-org](https://www.linkedin.com/company/kimai-org/)
|
||||
|
||||
### Credits
|
||||
|
||||
Kimai is based on modern technologies and frameworks such as [PHP](https://www.php.net/),
|
||||
|
||||
@@ -195,7 +195,7 @@ export default class KimaiCalendar {
|
||||
if (!this.isKimaiSource(unmountInfo.event)) {
|
||||
return;
|
||||
}
|
||||
const popover = Popover.getInstance(unmountInfo.element);
|
||||
const popover = Popover.getInstance(unmountInfo.el);
|
||||
if (popover !== null) {
|
||||
popover.dispose();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@import "variables";
|
||||
@import "layout";
|
||||
@import "error-page";
|
||||
@import "print";
|
||||
|
||||
33
assets/sass/variables.scss
Normal file
33
assets/sass/variables.scss
Normal file
@@ -0,0 +1,33 @@
|
||||
:root {
|
||||
--kimai-public-holiday: var(--tblr-lime);
|
||||
--kimai-holiday: var(--tblr-green);
|
||||
--kimai-sickness: var(--tblr-yellow);
|
||||
--kimai-time-off: var(--tblr-blue);
|
||||
--kimai-other: var(--tblr-purple);
|
||||
|
||||
--kimai-public-holiday-bg: var(--tblr-lime-lt);
|
||||
--kimai-holiday-bg: var(--tblr-green-lt);
|
||||
--kimai-sickness-bg: var(--tblr-yellow-lt);
|
||||
--kimai-time-off-bg: var(--tblr-blue-lt);
|
||||
--kimai-other-bg: var(--tblr-purple-lt);
|
||||
|
||||
--kimai-unexpected-bg: var(--tblr-pink-lt);
|
||||
--kimai-missing-bg: var(--tblr-pink-lt);
|
||||
--kimai-weekend-bg: var(--tblr-bg-surface-tertiary);
|
||||
}
|
||||
|
||||
.public-holiday { color: var(--kimai-public-holiday); }
|
||||
.holiday { color: var(--kimai-holiday); }
|
||||
.sickness { color: var(--kimai-sickness); }
|
||||
.time-off { color: var(--kimai-time-off); }
|
||||
.other { color: var(--kimai-other); }
|
||||
|
||||
.bg-public-holiday{ background-color: var(--kimai-public-holiday-bg); --tblr-table-bg: var(--kimai-public-holiday-bg); i.fas{ color: var(--kimai-public-holiday); } };
|
||||
.bg-holiday { background-color: var(--kimai-holiday-bg); --tblr-table-bg: var(--kimai-holiday-bg); i.fas{ color: var(--kimai-holiday); } };
|
||||
.bg-sickness { background-color: var(--kimai-sickness-bg); --tblr-table-bg: var(--kimai-sickness-bg); i.fas{ color: var(--kimai-sickness); } };
|
||||
.bg-time-off { background-color: var(--kimai-time-off-bg); --tblr-table-bg: var(--kimai-time-off-bg); i.fas{ color: var(--kimai-time-off); } };
|
||||
.bg-other { background-color: var(--kimai-other-bg); --tblr-table-bg: var(--kimai-other-bg); i.fas{ color: var(--kimai-other); } };
|
||||
|
||||
.bg-unexpected { background-color: var(--kimai-unexpected-bg); --tblr-table-bg: var(--kimai-unexpected-bg); };
|
||||
.bg-missing { background-color: var(--kimai-missing-bg); --tblr-table-bg: var(--kimai-missing-bg); };
|
||||
.bg-weekend { background-color: var(--kimai-weekend-bg); --tblr-table-bg: var(--kimai-weekend-bg); };
|
||||
897
composer.lock
generated
897
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -75,6 +75,7 @@ tabler:
|
||||
fax: fas fa-fax
|
||||
filter: fas fa-filter
|
||||
help: far fa-question-circle
|
||||
holiday: fas fa-umbrella-beach
|
||||
home: fas fa-home
|
||||
info: fas fa-info-circle
|
||||
import: fas fa-file-import
|
||||
@@ -95,6 +96,7 @@ tabler:
|
||||
ods: fas fa-table
|
||||
off: fas fa-toggle-off
|
||||
on: fas fa-toggle-on
|
||||
other: fas fa-file-alt
|
||||
password: fas fa-key
|
||||
pause: fas fa-pause
|
||||
pause-small: far fa-pause-circle
|
||||
@@ -108,6 +110,7 @@ tabler:
|
||||
profile: fas fa-user-edit
|
||||
profile-stats: far fa-chart-bar
|
||||
project: fas fa-briefcase
|
||||
public-holiday: fas fa-calendar-day
|
||||
repeat: fas fa-repeat
|
||||
reporting: far fa-chart-bar
|
||||
report: far fa-chart-bar
|
||||
@@ -119,6 +122,7 @@ tabler:
|
||||
save: far fa-save
|
||||
search: fas fa-search
|
||||
settings: fas fa-cog
|
||||
sickness: fas fa-prescription-bottle-medical
|
||||
shop: fas fa-shopping-cart
|
||||
spinner: fas fa-spinner
|
||||
start: fas fa-play
|
||||
@@ -130,6 +134,7 @@ tabler:
|
||||
team: fas fa-users
|
||||
timesheet: fas fa-clock
|
||||
timesheet-team: fas fa-user-clock
|
||||
time-off: fas fa-couch
|
||||
trash: far fa-trash-alt
|
||||
unlocked: fas fa-unlock-alt
|
||||
upload: fas fa-upload
|
||||
|
||||
@@ -27,6 +27,7 @@ parameters:
|
||||
numericOperandsInArithmeticOperators: true
|
||||
switchConditionsMatchingType: true
|
||||
noVariableVariables: false
|
||||
reportNonIntStringArrayKey: false
|
||||
paths:
|
||||
- src
|
||||
tmpDir: %rootDir%/../../../var/cache/phpstan
|
||||
@@ -3799,11 +3800,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Utils/ParsedownExtension.php
|
||||
|
||||
-
|
||||
message: "#^Property App\\\\Utils\\\\ParsedownExtension\\:\\:\\$safeLinksWhitelist has no type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/ParsedownExtension.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$profile of method App\\\\Utils\\\\ProfileManager\\:\\:getProfile\\(\\) expects string, mixed given\\.$#"
|
||||
count: 1
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@
|
||||
"/build/app.41748d55.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app.e64b4d08.css"
|
||||
"/build/app.6bfb9c66.css"
|
||||
]
|
||||
},
|
||||
"app-rtl": {
|
||||
@@ -15,7 +15,7 @@
|
||||
"/build/app-rtl.15853b82.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app-rtl.6b5a0ee9.css"
|
||||
"/build/app-rtl.16271884.css"
|
||||
]
|
||||
},
|
||||
"export-pdf": {
|
||||
@@ -54,7 +54,7 @@
|
||||
"calendar": {
|
||||
"js": [
|
||||
"/build/runtime.684e9f6d.js",
|
||||
"/build/calendar.36a92145.js"
|
||||
"/build/calendar.7e3186b2.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/calendar.d757753e.css"
|
||||
@@ -82,9 +82,9 @@
|
||||
"integrity": {
|
||||
"/build/runtime.684e9f6d.js": "sha384-suKiEX2de4fdNqQzdYbUd6osp4AepD9FiMXl+1QdvgMW9dcQqUWQNQasf3KWzwLr",
|
||||
"/build/app.41748d55.js": "sha384-iVPHnOXYjVDuq8567h8I3U2gl5SCV3mNMFNQCJdgMiNuFMZ4KO7PXOLanjgcGzTt",
|
||||
"/build/app.e64b4d08.css": "sha384-Rapl6RNn58g7oayglp7MWOER7tDNPpChx6RKOOWe0mg6uFgaJjy3M51ZSkxXm+Hg",
|
||||
"/build/app.6bfb9c66.css": "sha384-38+zSolYBrZdSwNcrDi5qCaQGCQlBJRKwtsJV+pnrf8rCCpf6z/M7fEpV4tS79Sz",
|
||||
"/build/app-rtl.15853b82.js": "sha384-UnKKgLMu9FnRT+CFE0no/+UiUks012bYriQdUWa6f02mo6Lswl947mPybjvKL503",
|
||||
"/build/app-rtl.6b5a0ee9.css": "sha384-eW8BEaA6S1jAFPB8lUCBTvcm4wZZQ3SHbiHVZlxe814TXGT9L4/4BN8CuuGehZ6o",
|
||||
"/build/app-rtl.16271884.css": "sha384-P1x4vYfjfM4JL55NQ3sfLiy+BSy8cM4bNcshj9M62nMJ4JD2xAWUioNv6VrmaK7U",
|
||||
"/build/export-pdf.395749ab.js": "sha384-3Hjvmu4FC/0dhHnR8kyRBU7k2xMNy1lxBpGgOkrw8PxXnwyQDM8/5bQmkJbjVT1+",
|
||||
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
||||
"/build/invoice.42b319e4.js": "sha384-xxK7sCe/ZhTjMPFPeX1xvILURxNRZz2hJHZxAGVaw9zE6TC++2/6y2eKqg8cz852",
|
||||
@@ -92,7 +92,7 @@
|
||||
"/build/invoice-pdf.26d98626.js": "sha384-gwNzQiU1y6qU/M9DPGiNW0MVZkLctEHk37sCES2X9ov+zugEaDABdkMjKBYOC9lz",
|
||||
"/build/invoice-pdf.2b749265.css": "sha384-DXXgkz2WWnrWnfBnXX5fmfPQSPb98upMnWxYKwTGYS04EhrPIWfDCutB2unIrWh7",
|
||||
"/build/chart.56f16a68.js": "sha384-SWnYjAbZ8OWEvTP+IZfGuBMWJIcH9OZWLYQ4p38KMPmqfufsc2zhzqWkALO3mCBO",
|
||||
"/build/calendar.36a92145.js": "sha384-ZPLxxbep6OnPeUiIeBXRE1iscZFgbNuTpn3rx2Jpf1KR2l6sxIuNsieDUskbmQ3m",
|
||||
"/build/calendar.7e3186b2.js": "sha384-umm2cw67dX5I6+PJ+PpNuHwWgKDJc+r2MmpN8az3dtAqUGa3N9ZGHD0SHc7xfFVB",
|
||||
"/build/calendar.d757753e.css": "sha384-cTmQMgHYjd2gfObFWmEUph7qQLCyXaIkneSf+bQ2mqVmZwqOB+pJOm/UYTyTjALJ",
|
||||
"/build/dashboard.9708ae5e.js": "sha384-QN7XIQuxFZu76sHVrgdpZL81+Q2VTwcgF3aI2CnpwYZiMoPbzrcTGfPL3RDy696t",
|
||||
"/build/dashboard.b7129fa1.css": "sha384-2nn5hLA+3YedgHYBpge62S8Losj8aoPwK9Zk9EvN1xEYatvOUQ7H3rIR2UUJAGOS",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"build/app.css": "/build/app.e64b4d08.css",
|
||||
"build/app.css": "/build/app.6bfb9c66.css",
|
||||
"build/app.js": "/build/app.41748d55.js",
|
||||
"build/app-rtl.css": "/build/app-rtl.6b5a0ee9.css",
|
||||
"build/app-rtl.css": "/build/app-rtl.16271884.css",
|
||||
"build/app-rtl.js": "/build/app-rtl.15853b82.js",
|
||||
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
||||
"build/export-pdf.js": "/build/export-pdf.395749ab.js",
|
||||
@@ -11,7 +11,7 @@
|
||||
"build/invoice-pdf.js": "/build/invoice-pdf.26d98626.js",
|
||||
"build/chart.js": "/build/chart.56f16a68.js",
|
||||
"build/calendar.css": "/build/calendar.d757753e.css",
|
||||
"build/calendar.js": "/build/calendar.36a92145.js",
|
||||
"build/calendar.js": "/build/calendar.7e3186b2.js",
|
||||
"build/dashboard.css": "/build/dashboard.b7129fa1.css",
|
||||
"build/dashboard.js": "/build/dashboard.9708ae5e.js",
|
||||
"build/highlight.css": "/build/highlight.98bf3927.css",
|
||||
|
||||
@@ -18,6 +18,7 @@ use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
@@ -76,6 +77,9 @@ final class InvoiceController extends BaseApiController
|
||||
/** @var array<int> $customers */
|
||||
$customers = $paramFetcher->get('customers');
|
||||
foreach ($customerRepository->findByIds(array_unique($customers)) as $customer) {
|
||||
if (!$this->isGranted('access', $customer)) {
|
||||
throw $this->createAccessDeniedException('Cannot access Customer: ' . $customer->getId());
|
||||
}
|
||||
$query->addCustomer($customer);
|
||||
}
|
||||
|
||||
@@ -90,6 +94,7 @@ final class InvoiceController extends BaseApiController
|
||||
* Fetch invoice
|
||||
*/
|
||||
#[IsGranted('view_invoice')]
|
||||
#[IsGranted(new Expression("is_granted('access', subject.getCustomer())"), 'invoice')]
|
||||
#[OA\Response(response: 200, description: 'Returns one invoice', content: new OA\JsonContent(ref: '#/components/schemas/Invoice'))]
|
||||
#[Route(methods: ['GET'], path: '/{id}', name: 'get_invoice', requirements: ['id' => '\d+'])]
|
||||
public function getAction(Invoice $invoice): Response
|
||||
|
||||
@@ -17,11 +17,11 @@ final class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.47.0';
|
||||
public const VERSION = '2.51.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 24700;
|
||||
public const VERSION_ID = 25100;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -108,10 +108,30 @@ final class DoctorController extends AbstractController
|
||||
'logLines' => $logLines,
|
||||
'logSize' => $this->getLogSize(),
|
||||
'composer' => $this->getComposerPackages(),
|
||||
'release' => $latestRelease
|
||||
'release' => $latestRelease,
|
||||
'opcache' => $this->getOpcacheConfiguration()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{enabled: bool, status: false|array<mixed>}
|
||||
*/
|
||||
private function getOpcacheConfiguration(): array
|
||||
{
|
||||
$status = \function_exists('opcache_get_status') ? opcache_get_status() : false;
|
||||
|
||||
$enabled = \is_array($status) && $status['opcache_enabled'];
|
||||
|
||||
if ($enabled && \array_key_exists('scripts', $status)) {
|
||||
unset($status['scripts']);
|
||||
}
|
||||
|
||||
return [
|
||||
'enabled' => $enabled,
|
||||
'status' => $status,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
@@ -264,7 +284,12 @@ final class DoctorController extends AbstractController
|
||||
'sys_temp_dir',
|
||||
'date.timezone',
|
||||
'session.gc_maxlifetime',
|
||||
'disable_functions'
|
||||
'disable_functions',
|
||||
'opcache.enable',
|
||||
'opcache.memory_consumption',
|
||||
'opcache.interned_strings_buffer',
|
||||
'opcache.max_accelerated_files',
|
||||
'opcache.validate_timestamps',
|
||||
];
|
||||
|
||||
$settings = [];
|
||||
|
||||
@@ -16,6 +16,7 @@ use App\Model\DateStatisticInterface;
|
||||
use App\Model\Statistic\StatisticDate;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\TimesheetStatisticQuery;
|
||||
use App\Timesheet\TimesheetStatisticService;
|
||||
use DateTimeInterface;
|
||||
|
||||
@@ -37,7 +38,7 @@ abstract class AbstractUserReportController extends AbstractController
|
||||
|
||||
protected function getStatisticDataRaw(DateTimeInterface $begin, DateTimeInterface $end, User $user): array
|
||||
{
|
||||
return $this->statisticService->getDailyStatisticsGrouped($begin, $end, [$user]);
|
||||
return $this->statisticService->getDailyStatisticsGrouped(new TimesheetStatisticQuery($begin, $end, [$user]));
|
||||
}
|
||||
|
||||
protected function createStatisticModel(DateTimeInterface $begin, DateTimeInterface $end, User $user): DateStatisticInterface
|
||||
|
||||
@@ -17,6 +17,7 @@ use App\Model\DateStatisticInterface;
|
||||
use App\Model\MonthlyStatistic;
|
||||
use App\Reporting\YearByUser\YearByUser;
|
||||
use App\Reporting\YearByUser\YearByUserForm;
|
||||
use App\Repository\Query\TimesheetStatisticQuery;
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||
@@ -125,7 +126,7 @@ final class UserYearController extends AbstractUserReportController
|
||||
|
||||
protected function getStatisticDataRaw(DateTimeInterface $begin, DateTimeInterface $end, User $user): array
|
||||
{
|
||||
return $this->statisticService->getMonthlyStatisticsGrouped($begin, $end, [$user]);
|
||||
return $this->statisticService->getMonthlyStatisticsGrouped(new TimesheetStatisticQuery($begin, $end, [$user]));
|
||||
}
|
||||
|
||||
protected function createStatisticModel(DateTimeInterface $begin, DateTimeInterface $end, User $user): DateStatisticInterface
|
||||
|
||||
@@ -256,6 +256,18 @@ class ExportTemplate
|
||||
return \is_string($font) ? $font : null;
|
||||
}
|
||||
|
||||
public function isAvailableForAll(): bool
|
||||
{
|
||||
$isAllowed = $this->getOption('user_access', false);
|
||||
|
||||
return \is_bool($isAllowed) ? $isAllowed : false;
|
||||
}
|
||||
|
||||
public function setAvailableForAll(bool $userAccess): void
|
||||
{
|
||||
$this->setOption('user_access', $userAccess);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->title ?? 'New';
|
||||
|
||||
@@ -36,6 +36,7 @@ class WorkingTime
|
||||
#[ORM\Column(name: 'expected', type: Types::INTEGER, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private int $expectedTime = 0;
|
||||
private ?int $originalExpectedTime = null;
|
||||
#[ORM\Column(name: 'actual', type: Types::INTEGER, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private int $actualTime = 0;
|
||||
@@ -75,6 +76,39 @@ class WorkingTime
|
||||
public function setExpectedTime(int $expectedTime): void
|
||||
{
|
||||
$this->expectedTime = $expectedTime;
|
||||
$this->storeOriginalExpectedTime();
|
||||
}
|
||||
|
||||
public function storeOriginalExpectedTime(): void
|
||||
{
|
||||
if ($this->originalExpectedTime === null) {
|
||||
$this->originalExpectedTime = $this->expectedTime;
|
||||
}
|
||||
}
|
||||
|
||||
public function getOriginalExpectedTime(): int
|
||||
{
|
||||
$this->storeOriginalExpectedTime();
|
||||
|
||||
return $this->originalExpectedTime ?? 0;
|
||||
}
|
||||
|
||||
public function halveExpectedTime(): int
|
||||
{
|
||||
$this->storeOriginalExpectedTime();
|
||||
|
||||
$reduceBy = ($this->getOriginalExpectedTime() / 2);
|
||||
|
||||
$this->expectedTime = $this->expectedTime - $reduceBy;
|
||||
|
||||
return $reduceBy;
|
||||
}
|
||||
|
||||
public function emptyExpectedTime(): void
|
||||
{
|
||||
$this->storeOriginalExpectedTime();
|
||||
|
||||
$this->expectedTime = 0;
|
||||
}
|
||||
|
||||
public function getActualTime(): int
|
||||
|
||||
25
src/Event/TimesheetStatisticsQueryEvent.php
Normal file
25
src/Event/TimesheetStatisticsQueryEvent.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class TimesheetStatisticsQueryEvent extends Event
|
||||
{
|
||||
public function __construct(private readonly QueryBuilder $queryBuilder)
|
||||
{
|
||||
}
|
||||
|
||||
public function getQueryBuilder(): QueryBuilder
|
||||
{
|
||||
return $this->queryBuilder;
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ final class TimesheetsSubscriber extends AbstractActionsSubscriber
|
||||
|
||||
if ($this->isGranted('export_own_timesheet')) {
|
||||
foreach ($this->serviceExport->getTimesheetExporter() as $exporter) {
|
||||
$event->addActionToSubmenu('export', $exporter->getId(), ['url' => $this->path('timesheet_export', ['exporter' => $exporter->getId()]), 'class' => 'toolbar-action', 'title' => 'button.' . $exporter->getId(), 'translation_domain' => 'messages']);
|
||||
$event->addActionToSubmenu('export', $exporter->getId(), ['url' => $this->path('timesheet_export', ['exporter' => $exporter->getId()]), 'class' => 'toolbar-action', 'title' => $exporter->getTitle()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ final class TimesheetsTeamSubscriber extends AbstractActionsSubscriber
|
||||
|
||||
if ($this->isGranted('export_other_timesheet')) {
|
||||
foreach ($this->serviceExport->getTimesheetExporter() as $exporter) {
|
||||
$event->addActionToSubmenu('export', $exporter->getId(), ['url' => $this->path('admin_timesheet_export', ['exporter' => $exporter->getId()]), 'class' => 'toolbar-action', 'title' => 'button.' . $exporter->getId(), 'translation_domain' => 'messages']);
|
||||
$event->addActionToSubmenu('export', $exporter->getId(), ['url' => $this->path('admin_timesheet_export', ['exporter' => $exporter->getId()]), 'class' => 'toolbar-action', 'title' => $exporter->getTitle()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ final class CsvRendererFactory
|
||||
|
||||
public function createDefault(): CsvRenderer
|
||||
{
|
||||
$template = new DefaultTemplate($this->eventDispatcher, 'csv');
|
||||
$template = new DefaultTemplate($this->eventDispatcher, 'csv', 'en', 'csv');
|
||||
|
||||
return new CsvRenderer($this->converter, $this->translator, $template);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ final class HtmlRendererFactory
|
||||
) {
|
||||
}
|
||||
|
||||
public function create(string $id, string $template): HtmlRenderer
|
||||
public function create(string $id, string $template, string $title = 'print'): HtmlRenderer
|
||||
{
|
||||
return new HtmlRenderer(
|
||||
$this->twig,
|
||||
@@ -33,7 +33,7 @@ final class HtmlRendererFactory
|
||||
$this->projectStatisticService,
|
||||
$this->activityStatisticService,
|
||||
$id,
|
||||
'print',
|
||||
$title,
|
||||
$template
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ final class XlsxRendererFactory
|
||||
|
||||
public function createDefault(): XlsxRenderer
|
||||
{
|
||||
$template = new DefaultTemplate($this->eventDispatcher, 'xlsx');
|
||||
$template = new DefaultTemplate($this->eventDispatcher, 'xlsx', 'en', 'xlsx');
|
||||
|
||||
return new XlsxRenderer($this->converter, $this->translator, $template);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Export;
|
||||
|
||||
use App\Entity\ExportableItem;
|
||||
use App\Entity\ExportTemplate;
|
||||
use App\Event\ExportItemsQueryEvent;
|
||||
use App\Export\Renderer\CsvRendererFactory;
|
||||
use App\Export\Renderer\HtmlRendererFactory;
|
||||
@@ -74,6 +75,15 @@ final class ServiceExport
|
||||
$this->renderer[] = $renderer;
|
||||
}
|
||||
|
||||
private function filenameToTitle(string $title): string
|
||||
{
|
||||
if (str_contains($title, '.')) {
|
||||
$title = explode('.', $title)[0];
|
||||
}
|
||||
|
||||
return str_replace(['-', '_'], ' ', $title);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ExportRendererInterface[]
|
||||
*/
|
||||
@@ -82,32 +92,15 @@ final class ServiceExport
|
||||
$renderer = [
|
||||
$this->csvRendererFactory->createDefault(),
|
||||
$this->xlsxRendererFactory->createDefault(),
|
||||
$this->pdfRendererFactory->create('pdf', 'export/pdf-layout.html.twig', 'default'),
|
||||
$this->htmlRendererFactory->create('html', 'export/print.html.twig'),
|
||||
$this->pdfRendererFactory->create('pdf', 'export/pdf-layout.html.twig', 'pdf'),
|
||||
$this->htmlRendererFactory->create('print', 'export/print.html.twig'),
|
||||
];
|
||||
|
||||
foreach ($this->exportTemplateRepository->findAll() as $template) {
|
||||
$tpl = new Template((string) $template->getId(), $template->getTitle()); // @phpstan-ignore argument.type
|
||||
$tpl->setColumns($template->getColumns());
|
||||
$tpl->setLocale($template->getLanguage());
|
||||
$tpl->setOptions($template->getOptions());
|
||||
|
||||
switch ($template->getRenderer()) {
|
||||
case 'csv':
|
||||
$renderer[] = $this->csvRendererFactory->create($tpl);
|
||||
break;
|
||||
|
||||
case 'xlsx':
|
||||
$renderer[] = $this->xlsxRendererFactory->create($tpl);
|
||||
break;
|
||||
|
||||
case 'pdf':
|
||||
$renderer[] = $this->pdfRendererFactory->createFromTemplate($tpl);
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->logger->error('Unknown export template type: ' . $template->getRenderer());
|
||||
break;
|
||||
try {
|
||||
$renderer[] = $this->createTemplateFromExportTemplate($template);
|
||||
} catch (\Exception $exception) {
|
||||
$this->logger->error('Unknown export template type: ' . $template->getRenderer());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +117,7 @@ final class ServiceExport
|
||||
continue;
|
||||
}
|
||||
|
||||
$renderer[] = $this->htmlRendererFactory->create($tplName, '@export/' . $tplName);
|
||||
$renderer[] = $this->htmlRendererFactory->create($tplName, '@export/' . $tplName, $this->filenameToTitle($tplName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +129,7 @@ final class ServiceExport
|
||||
continue;
|
||||
}
|
||||
|
||||
$renderer[] = $this->pdfRendererFactory->create($tplName, '@export/' . $tplName);
|
||||
$renderer[] = $this->pdfRendererFactory->create($tplName, '@export/' . $tplName, $this->filenameToTitle($tplName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,6 +137,28 @@ final class ServiceExport
|
||||
return array_merge($this->renderer, $renderer);
|
||||
}
|
||||
|
||||
private function createTemplateFromExportTemplate(ExportTemplate $template): ExportRendererInterface
|
||||
{
|
||||
$tpl = new Template((string) $template->getId(), $template->getTitle()); // @phpstan-ignore argument.type
|
||||
$tpl->setColumns($template->getColumns());
|
||||
$tpl->setLocale($template->getLanguage());
|
||||
$tpl->setOptions($template->getOptions());
|
||||
|
||||
switch ($template->getRenderer()) {
|
||||
case 'csv':
|
||||
return $this->csvRendererFactory->create($tpl);
|
||||
|
||||
case 'xlsx':
|
||||
return $this->xlsxRendererFactory->create($tpl);
|
||||
|
||||
case 'pdf':
|
||||
return $this->pdfRendererFactory->createFromTemplate($tpl);
|
||||
|
||||
default:
|
||||
throw new \Exception('Unknown export template type: ' . $template->getRenderer());
|
||||
}
|
||||
}
|
||||
|
||||
public function getRendererById(string $id): ?ExportRendererInterface
|
||||
{
|
||||
foreach ($this->getRenderer() as $renderer) {
|
||||
@@ -165,6 +180,7 @@ final class ServiceExport
|
||||
*/
|
||||
public function getTimesheetExporter(): array
|
||||
{
|
||||
// TODO 3.0 cache the result, as this is one extra database query on the timesheet pages
|
||||
$exporter = [
|
||||
$this->pdfRendererFactory->create('pdf', '@export/timesheet.pdf.twig'),
|
||||
$this->xlsxRendererFactory->createDefault(),
|
||||
@@ -172,6 +188,17 @@ final class ServiceExport
|
||||
$this->htmlRendererFactory->create('print', 'timesheet/export.html.twig'),
|
||||
];
|
||||
|
||||
foreach ($this->exportTemplateRepository->findAll() as $template) {
|
||||
if (!$template->isAvailableForAll()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$exporter[] = $this->createTemplateFromExportTemplate($template);
|
||||
} catch (\Exception $exception) {
|
||||
$this->logger->error('Unknown export template type: ' . $template->getRenderer());
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge($this->timesheetExporter, $exporter);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Form\Type\ExportRendererType;
|
||||
use App\Form\Type\ExportSummaryColumnsType;
|
||||
use App\Form\Type\LanguageType;
|
||||
use App\Form\Type\PdfFontType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
@@ -22,6 +23,9 @@ use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\Length;
|
||||
|
||||
/**
|
||||
* TODO rename with 3.0 to ExportTemplateForm
|
||||
*/
|
||||
class ExportTemplateSpreadsheetForm extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
@@ -77,6 +81,11 @@ class ExportTemplateSpreadsheetForm extends AbstractType
|
||||
'row_attr' => ['data-type' => 'pdf'],
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
$builder->add('availableForAll', YesNoType::class, [
|
||||
'label' => 'user_access_all',
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
|
||||
@@ -19,9 +19,9 @@ final class ExportRendererType extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'choices' => [
|
||||
'button.csv' => 'csv',
|
||||
'button.xlsx' => 'xlsx',
|
||||
'button.pdf' => 'pdf'
|
||||
'csv' => 'csv',
|
||||
'xlsx' => 'xlsx',
|
||||
'pdf' => 'pdf'
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ final class QuickEntryTimesheetType extends AbstractType
|
||||
$duration = $data->getDuration(false);
|
||||
try {
|
||||
if (null !== $duration) {
|
||||
$duration += $data->getBreak();
|
||||
$end = clone $data->getBegin();
|
||||
$end->modify('+ ' . abs($duration) . ' seconds');
|
||||
$data->setEnd($end);
|
||||
|
||||
@@ -33,8 +33,9 @@ final class TeamType extends AbstractType
|
||||
return $team->getName();
|
||||
},
|
||||
'documentation' => [
|
||||
'type' => 'integer',
|
||||
'description' => 'Team ID',
|
||||
'type' => 'array',
|
||||
'items' => ['type' => 'integer', 'description' => 'Team IDs'],
|
||||
'description' => 'Array of Team IDs',
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
@@ -10,15 +10,19 @@
|
||||
namespace App\Timesheet;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\TimesheetStatisticsQueryEvent;
|
||||
use App\Model\DailyStatistic;
|
||||
use App\Model\MonthlyStatistic;
|
||||
use App\Repository\Query\TimesheetStatisticQuery;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use DateTimeInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
final class TimesheetStatisticService
|
||||
{
|
||||
public function __construct(private readonly TimesheetRepository $repository)
|
||||
public function __construct(
|
||||
private readonly TimesheetRepository $repository,
|
||||
private readonly EventDispatcherInterface $eventDispatcher
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -72,6 +76,8 @@ final class TimesheetStatisticService
|
||||
;
|
||||
}
|
||||
|
||||
$this->eventDispatcher->dispatch(new TimesheetStatisticsQueryEvent($qb));
|
||||
|
||||
$results = $qb->getQuery()->getResult();
|
||||
|
||||
foreach ($results as $row) {
|
||||
@@ -96,13 +102,14 @@ final class TimesheetStatisticService
|
||||
|
||||
/**
|
||||
* @internal only for core development
|
||||
* @param DateTimeInterface $begin
|
||||
* @param DateTimeInterface $end
|
||||
* @param User[] $users
|
||||
* @return array
|
||||
*/
|
||||
public function getDailyStatisticsGrouped(DateTimeInterface $begin, DateTimeInterface $end, array $users): array
|
||||
public function getDailyStatisticsGrouped(TimesheetStatisticQuery $query): array
|
||||
{
|
||||
$begin = $query->getBegin();
|
||||
$end = $query->getEnd();
|
||||
$users = $query->getUsers();
|
||||
|
||||
$stats = [];
|
||||
$usersById = [];
|
||||
|
||||
@@ -137,6 +144,8 @@ final class TimesheetStatisticService
|
||||
->addGroupBy('billable')
|
||||
;
|
||||
|
||||
$this->eventDispatcher->dispatch(new TimesheetStatisticsQueryEvent($qb));
|
||||
|
||||
$results = $qb->getQuery()->getResult();
|
||||
|
||||
foreach ($results as $row) {
|
||||
@@ -173,11 +182,14 @@ final class TimesheetStatisticService
|
||||
|
||||
/**
|
||||
* @internal only for core development
|
||||
* @param User[] $users
|
||||
* @return array
|
||||
*/
|
||||
public function getMonthlyStatisticsGrouped(DateTimeInterface $begin, DateTimeInterface $end, array $users): array
|
||||
public function getMonthlyStatisticsGrouped(TimesheetStatisticQuery $query): array
|
||||
{
|
||||
$begin = $query->getBegin();
|
||||
$end = $query->getEnd();
|
||||
$users = $query->getUsers();
|
||||
|
||||
$stats = [];
|
||||
$usersById = [];
|
||||
|
||||
@@ -214,6 +226,8 @@ final class TimesheetStatisticService
|
||||
->addGroupBy('billable')
|
||||
;
|
||||
|
||||
$this->eventDispatcher->dispatch(new TimesheetStatisticsQueryEvent($qb));
|
||||
|
||||
$results = $qb->getQuery()->getResult();
|
||||
|
||||
foreach ($results as $row) {
|
||||
@@ -311,6 +325,8 @@ final class TimesheetStatisticService
|
||||
;
|
||||
}
|
||||
|
||||
$this->eventDispatcher->dispatch(new TimesheetStatisticsQueryEvent($qb));
|
||||
|
||||
$results = $qb->getQuery()->getResult();
|
||||
|
||||
foreach ($results as $row) {
|
||||
|
||||
42
src/Twig/SecurityPolicy/DefaultPolicy.php
Normal file
42
src/Twig/SecurityPolicy/DefaultPolicy.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig\SecurityPolicy;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Exclude;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
/**
|
||||
* @deprecated since 2.47.0 - use StrictPolicy instead
|
||||
*/
|
||||
#[Exclude]
|
||||
final class DefaultPolicy implements SecurityPolicyInterface
|
||||
{
|
||||
private SecurityPolicyInterface $securityPolicy;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->securityPolicy = new StrictPolicy();
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
$this->securityPolicy->checkSecurity($tags, $filters, $functions);
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
$this->securityPolicy->checkMethodAllowed($obj, $method);
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
$this->securityPolicy->checkPropertyAllowed($obj, $property);
|
||||
}
|
||||
}
|
||||
42
src/Twig/SecurityPolicy/ExportPolicy.php
Normal file
42
src/Twig/SecurityPolicy/ExportPolicy.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig\SecurityPolicy;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Exclude;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
/**
|
||||
* @deprecated since 2.47.0 - use StrictPolicy instead
|
||||
*/
|
||||
#[Exclude]
|
||||
final class ExportPolicy implements SecurityPolicyInterface
|
||||
{
|
||||
private SecurityPolicyInterface $securityPolicy;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->securityPolicy = new StrictPolicy();
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
$this->securityPolicy->checkSecurity($tags, $filters, $functions);
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
$this->securityPolicy->checkMethodAllowed($obj, $method);
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
$this->securityPolicy->checkPropertyAllowed($obj, $property);
|
||||
}
|
||||
}
|
||||
42
src/Twig/SecurityPolicy/InvoicePolicy.php
Normal file
42
src/Twig/SecurityPolicy/InvoicePolicy.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig\SecurityPolicy;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Exclude;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
/**
|
||||
* @deprecated since 2.47.0 - use StrictPolicy instead
|
||||
*/
|
||||
#[Exclude]
|
||||
final class InvoicePolicy implements SecurityPolicyInterface
|
||||
{
|
||||
private SecurityPolicyInterface $securityPolicy;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->securityPolicy = new StrictPolicy();
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
$this->securityPolicy->checkSecurity($tags, $filters, $functions);
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
$this->securityPolicy->checkMethodAllowed($obj, $method);
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
$this->securityPolicy->checkPropertyAllowed($obj, $property);
|
||||
}
|
||||
}
|
||||
@@ -21,15 +21,17 @@ class Parsedown extends \Parsedown
|
||||
{
|
||||
$block = parent::blockHeader($Line);
|
||||
|
||||
$text = $block['element']['text'];
|
||||
if (isset($block['element']['handler']['argument'])) {
|
||||
$text = $block['element']['handler']['argument'];
|
||||
|
||||
if (\is_string($text) && $text !== '') {
|
||||
$id = $this->getIDfromText($text);
|
||||
if (\is_string($text) && $text !== '') {
|
||||
$id = $this->getIDfromText($text);
|
||||
|
||||
// add id-attribute
|
||||
$block['element']['attributes'] = [
|
||||
'id' => $id
|
||||
];
|
||||
// add id-attribute
|
||||
$block['element']['attributes'] = [
|
||||
'id' => $id
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $block;
|
||||
|
||||
@@ -44,55 +44,17 @@ final class ParsedownExtension extends Parsedown
|
||||
];
|
||||
|
||||
/**
|
||||
* Overwritten to add support for file:///
|
||||
*/
|
||||
protected $safeLinksWhitelist = [
|
||||
'file:///',
|
||||
'http://',
|
||||
'https://',
|
||||
'ftp://',
|
||||
'ftps://',
|
||||
'mailto:',
|
||||
'data:image/png;base64,',
|
||||
'data:image/gif;base64,',
|
||||
'data:image/jpeg;base64,',
|
||||
'irc:',
|
||||
'ircs:',
|
||||
'git:',
|
||||
'ssh:',
|
||||
'news:',
|
||||
'steam:',
|
||||
];
|
||||
|
||||
/**
|
||||
* Overwritten:
|
||||
* - added support for file:///
|
||||
* - open links in new windows
|
||||
* Overwritten to open links in new windows
|
||||
*/
|
||||
protected function inlineUrl($Excerpt): ?array
|
||||
{
|
||||
if ($this->urlsLinked !== true or !isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') {
|
||||
return null;
|
||||
$block = parent::inlineUrl($Excerpt);
|
||||
|
||||
if (isset($block['element']['attributes']) && \is_array($block['element']['attributes'])) {
|
||||
$block['element']['attributes']['target'] = '_blank';
|
||||
}
|
||||
|
||||
if (preg_match('/\b(https?:[\/]{2}|file:[\/]{3})[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) {
|
||||
$url = $matches[0][0];
|
||||
|
||||
return [
|
||||
'extent' => \strlen($matches[0][0]),
|
||||
'position' => $matches[0][1],
|
||||
'element' => [
|
||||
'name' => 'a',
|
||||
'text' => $url,
|
||||
'attributes' => [
|
||||
'href' => $url,
|
||||
'target' => '_blank'
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
return $block;
|
||||
}
|
||||
|
||||
protected function blockTable($Line, ?array $Block = null) // @phpstan-ignore missingType.return,missingType.iterableValue,missingType.parameter
|
||||
|
||||
@@ -59,6 +59,6 @@ final class UserDurationYear extends AbstractCounterYear
|
||||
|
||||
protected function getFinancialYearTitle(): string
|
||||
{
|
||||
return 'stats.durationFinancialYear';
|
||||
return 'stats.userDurationYear';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,12 @@ final class DayAddon
|
||||
{
|
||||
private bool $billable = true;
|
||||
|
||||
public function __construct(private string $title, private int $duration, private int $visibleDuration)
|
||||
public function __construct(
|
||||
private readonly string $title,
|
||||
private readonly int $duration,
|
||||
private readonly int $visibleDuration,
|
||||
private readonly ?string $type = null
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -41,4 +46,9 @@ final class DayAddon
|
||||
{
|
||||
return $this->visibleDuration;
|
||||
}
|
||||
|
||||
public function getType(): ?string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{% set currency = activity.project.customer.currency %}
|
||||
{% endif %}
|
||||
|
||||
{% embed '@theme/embeds/card.html.twig' %}
|
||||
{% embed '@theme/embeds/card.html.twig' with {fullsize: true} %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "customer/actions.html.twig" as customerActions %}
|
||||
{% import "project/actions.html.twig" as projectActions %}
|
||||
@@ -21,7 +21,6 @@
|
||||
{{ widgets.card_tool_button('edit', {'class': 'modal-ajax-form open-edit', 'title': 'edit', 'url': path('admin_activity_edit', {'id': activity.id})}) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block box_body_class %}p-0{% endblock %}
|
||||
{% block box_body %}
|
||||
{% if activity.comment is not empty %}
|
||||
<div class="comment p-3">
|
||||
|
||||
@@ -55,8 +55,7 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="col-xs-12 {% if hasTwoColumns %}col-md-8 col-lg-9 col-print-12{% endif %}">
|
||||
{% embed '@theme/embeds/card.html.twig' %}
|
||||
{% block box_body_class %}p-0{% endblock %}
|
||||
{% embed '@theme/embeds/card.html.twig' with {fullsize: true} %}
|
||||
{% block box_body %}
|
||||
<div id="timesheet_calendar"></div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -158,28 +158,35 @@
|
||||
</td>
|
||||
{% set dayCount = 0 %}
|
||||
{% for day in month.days %}
|
||||
{% set class = 'text-end contractDay text-nowrap' %}
|
||||
{% if day.workingTime.expectedTime == 0 %}
|
||||
{% set class = class ~ ' weekend' %}
|
||||
{% endif %}
|
||||
{% set style = null %}
|
||||
{% if day.workingTime.expectedTime > 0 and day.workingTime.actualTime == 0 and now > day.day %}
|
||||
{% set class = class ~ ' bg-orange-lt' %}
|
||||
{% set style = 'missing' %}
|
||||
{% elseif day.workingTime.expectedTime == 0 and day.workingTime.actualTime > 0 and now > day.day %}
|
||||
{% set class = class ~ ' bg-yellow-lt' %}
|
||||
{% set style = 'unexpected' %}
|
||||
{% endif %}
|
||||
<td class="{{ class }}">
|
||||
{% if (day.workingTime.expectedTime != 0 or day.workingTime.actualTime != 0) and (now > day.day or day.workingTime.actualTime > 0) %}
|
||||
{% if day.hasAddons() %}
|
||||
{% set statusTitle = '' %}
|
||||
{% for addon in day.getAddons() %}
|
||||
{% set statusTitle = statusTitle ~ (addon.title|trans) ~ ' (' ~ addon.visibleDuration|duration(decimal) ~ ')' %}
|
||||
{% if not loop.last %}
|
||||
{% set statusTitle = statusTitle ~ ', ' %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<span class="status-dot status-azure" data-toggle="tooltip" data-placement="top" title="{{ statusTitle }}"></span>
|
||||
{% set statusTitle = null %}
|
||||
{% if day.hasAddons() %}
|
||||
{% for addon in day.getAddons() %}
|
||||
{% set style = addon.type ?? style %}
|
||||
{% set statusTitle = (statusTitle ?? '') ~ (addon.title|trans) %}
|
||||
{% if addon.visibleDuration > 0 %}
|
||||
{% set statusTitle = statusTitle ~ ' (' ~ addon.visibleDuration|duration(decimal) ~ ')' %}
|
||||
{% endif %}
|
||||
{% if not loop.last %}
|
||||
{% set statusTitle = statusTitle ~ ', ' %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<td class="text-end contractDay text-nowrap{% if style is not null %} bg-{{ style }}{% endif %}"{% if statusTitle is not null %} data-toggle="tooltip" data-placement="top" title="{{ statusTitle }}"{% endif %}>
|
||||
{% if (day.workingTime.expectedTime != 0 or day.workingTime.actualTime != 0 or day.hasAddons()) and (now > day.day or day.workingTime.actualTime > 0) %}
|
||||
{% if style is not null and statusTitle is not null %}
|
||||
<i class="{{ style|icon }} fa-xs"></i>
|
||||
{% endif %}
|
||||
{% if day.workingTime.expectedTime == 0 and day.workingTime.actualTime == 0 %}
|
||||
--:--
|
||||
{% else %}
|
||||
{{ work_times_result(day.workingTime.expectedTime, day.workingTime.actualTime, decimal) }}
|
||||
{% endif %}
|
||||
{{ work_times_result(day.workingTime.expectedTime, day.workingTime.actualTime, decimal) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% set dayCount = dayCount + 1 %}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{% block main %}
|
||||
{% set can_edit = is_granted('edit', customer) %}
|
||||
|
||||
{% embed '@theme/embeds/card.html.twig' %}
|
||||
{% embed '@theme/embeds/card.html.twig' with {fullsize: true} %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_attributes %}id="customer_details_box"{% endblock %}
|
||||
{% block box_title %}
|
||||
@@ -16,7 +16,6 @@
|
||||
{{ widgets.card_tool_button('edit', {'class': 'modal-ajax-form open-edit', 'title': 'edit', 'url': path('admin_customer_edit', {'id': customer.id})}) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block box_body_class %}p-0{% endblock %}
|
||||
{% block box_body %}
|
||||
{% if customer.comment is not empty %}
|
||||
<div class="comment p-3">
|
||||
|
||||
@@ -162,4 +162,72 @@
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
{% set opcache_warning = not opcache['enabled'] %}
|
||||
{% embed '@theme/embeds/collapsible.html.twig' with {open: opcache_warning, boxtype: (opcache_warning ? 'danger' : 'success')} %}
|
||||
{% block title %}OPCache{% endblock %}
|
||||
{% block body_class %}p-0{% endblock %}
|
||||
{% block body %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% if opcache_warning %}
|
||||
{{ widgets.alert('danger', 'Could not detect an active OPCache. This will significantly impact the performance of Kimai.') }}
|
||||
{% endif %}
|
||||
{% if opcache['status'] is iterable %}
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th class="text-nowrap w-min">Opcache Enabled</th>
|
||||
<td class="text-break">{{ widgets.label_boolean(opcache['enabled']) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-nowrap w-min">Opcache Full</th>
|
||||
<td class="text-break">{{ widgets.label_boolean(opcache['status']['cache_full']) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-nowrap w-min">Memory Used</th>
|
||||
<td class="text-break">{{ (opcache['status']['memory_usage']['used_memory'] / 1024 / 1024)|number_format(2) }} MB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-nowrap w-min">Memory Free</th>
|
||||
<td class="text-break">{{ (opcache['status']['memory_usage']['free_memory'] / 1024 / 1024)|number_format(2) }} MB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-nowrap w-min">Interned-Strings Used</th>
|
||||
<td class="text-break">{{ (opcache['status']['interned_strings_usage']['used_memory'] / 1024 / 1024)|number_format(2) }} MB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-nowrap w-min">Interned-Strings Free</th>
|
||||
<td class="text-break">{{ (opcache['status']['interned_strings_usage']['free_memory'] / 1024 / 1024)|number_format(2) }} MB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-nowrap w-min">Cached Scripts</th>
|
||||
<td class="text-break">{{ opcache['status']['opcache_statistics']['num_cached_scripts'] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-nowrap w-min">Cached Keys</th>
|
||||
<td class="text-break">{{ opcache['status']['opcache_statistics']['num_cached_keys'] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-nowrap w-min">Max Keys</th>
|
||||
<td class="text-break">{{ opcache['status']['opcache_statistics']['max_cached_keys'] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-nowrap w-min">Hit Rate</th>
|
||||
<td class="text-break">{{ opcache['status']['opcache_statistics']['opcache_hit_rate']|number_format(2) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-nowrap w-min">Jit enabled</th>
|
||||
<td class="text-break">{{ widgets.label_boolean(opcache['status']['jit']['enabled']) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-nowrap w-min">Buffer Size</th>
|
||||
<td class="text-break">{{ (opcache['status']['jit']['buffer_size'] / 1024 / 1024)|number_format(2) }} MB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-nowrap w-min">Buffer Free</th>
|
||||
<td class="text-break">{{ (opcache['status']['jit']['buffer_free'] / 1024 / 1024)|number_format(2) }} MB</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
{% set timeBudgetVisible = is_granted('time', entity) %}
|
||||
{% set durationTrans = entity.isMonthlyBudget() ? 'stats.durationMonth' : 'stats.workingTime' %}
|
||||
{% if timeBudgetVisible %}
|
||||
{% embed '@theme/embeds/card.html.twig' %}
|
||||
{% embed '@theme/embeds/card.html.twig' with {fullsize: true} %}
|
||||
{% import "macros/progressbar.html.twig" as progress %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_title %}
|
||||
{{ 'timeBudget'|trans }} {% if entity.isMonthlyBudget() %}({{ 'budgetType_month'|trans }}){% endif %}
|
||||
{% endblock %}
|
||||
{% block box_body_class %}p-0{% endblock %}
|
||||
{% block box_attributes %}id="time_budget_box"{% endblock %}
|
||||
{% block box_body %}
|
||||
<div class="row">
|
||||
@@ -59,13 +58,12 @@
|
||||
{% endembed %}
|
||||
{% endif %}
|
||||
{% if is_granted('budget', entity) %}
|
||||
{% embed '@theme/embeds/card.html.twig' %}
|
||||
{% embed '@theme/embeds/card.html.twig' with {fullsize: true} %}
|
||||
{% import "macros/progressbar.html.twig" as progress %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_title %}
|
||||
{{ 'budget'|trans }} {% if entity.isMonthlyBudget() %}({{ 'budgetType_month'|trans }}){% endif %}
|
||||
{% endblock %}
|
||||
{% block box_body_class %}p-0{% endblock %}
|
||||
{% block box_attributes %}id="budget_box"{% endblock %}
|
||||
{% block box_body %}
|
||||
<div class="row">
|
||||
|
||||
@@ -104,13 +104,12 @@
|
||||
{% if entries is empty %}
|
||||
{{ widgets.nothing_found() }}
|
||||
{% else %}
|
||||
{% embed '@theme/embeds/card.html.twig' %}
|
||||
{% embed '@theme/embeds/card.html.twig' with {fullsize: true} %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_title %}
|
||||
{{ 'preview'|trans }}
|
||||
{% endblock %}
|
||||
{% block box_attributes %}id="preview_export"{% endblock %}
|
||||
{% block box_body_class %}p-0{% endblock %}
|
||||
{% block box_footer %}
|
||||
{% if showMarkAsExportedButton %}
|
||||
<div class="d-flex">
|
||||
@@ -124,17 +123,13 @@
|
||||
<div class="btn-group me-auto" id="export-buttons" role="group">
|
||||
{% set allowCreate = is_granted('create_export_template') %}
|
||||
{% for type, group in buttons %}
|
||||
{% set btnTitle = ('button.' ~ type)|trans %}
|
||||
{% if btnTitle == ('button.' ~ type) %}
|
||||
{% set btnTitle = type|trans %}
|
||||
{% if type == 'html' %}
|
||||
{% set type = 'print' %}
|
||||
{% endif %}
|
||||
{% set btnTitle = type|trans %}
|
||||
{% if group|length == 1 %}
|
||||
{% for id, cfg in group %}
|
||||
{% set title = cfg.title %}
|
||||
{% set btnTitle2 = ('button.' ~ title)|trans %}
|
||||
{% if btnTitle2 != ('button.' ~ title) %}
|
||||
{% set btnTitle = btnTitle2 %}
|
||||
{% endif %}
|
||||
<button type="button" id="export-{{ id }}-button" class="btn btn-primary startExportBtn" data-type="{{ id }}">
|
||||
{{ btnTitle }}
|
||||
</button>
|
||||
@@ -146,13 +141,9 @@
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
{% for id, cfg in group %}
|
||||
{% set title = cfg.title %}
|
||||
{% set btnTitle = id|trans({}, 'export') %}
|
||||
{% if btnTitle == id %}
|
||||
{% set btnTitle = title|trans %}
|
||||
{% endif %}
|
||||
{% if btnTitle == id %}
|
||||
{% set btnTitle = id|split('.')|first|replace({'-': ' ', '_': ' '})|split(' ')|map(t => t|capitalize)|join(' ') %}
|
||||
{% set btnTitle = (cfg.title)|trans %}
|
||||
{% if loop.first %}
|
||||
{% set btnTitle = 'default'|trans %}
|
||||
{% endif %}
|
||||
<a href="#" class="dropdown-item startExportBtn" data-type="{{ id }}">
|
||||
{{ btnTitle }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ app.locale }}">
|
||||
{% from "macros/meta_fields.html.twig" import meta_field_value %}
|
||||
{% from "macros/meta_fields.html.twig" import meta_field_value, form_type_value %}
|
||||
{% set storageItemName = 'kimai_html_export' %}
|
||||
{% set decimal = decimal|default(false) %}
|
||||
{% set columnTitles = {} %}
|
||||
@@ -507,7 +507,7 @@
|
||||
<td class="column-{{ 'u_' ~ field.name }} text-nowrap" {% if not columns['u_' ~ field.name] %}style="display: none"{% endif %}>
|
||||
{% set metaField = entry.user.preference(field.name) %}
|
||||
{% if not metaField is null and metaField.value is not null and metaField.value is not empty %}
|
||||
{{ _self.form_type_value(field.type, metaField.value, entry.user) }}
|
||||
{{ form_type_value(field.type, metaField.value, entry.user) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{% set showCustomerSummary = false %}
|
||||
{% set decimal = true %}
|
||||
{% set showDateTimeShort = true %}
|
||||
{% block title %}{{ 'timesheet.pdf.twig'|trans({}, 'export') }}{% endblock %}
|
||||
{% block title %}{{ 'timesheet'|trans }}{% endblock %}
|
||||
{% block description %}
|
||||
{% if entry.description is not empty %}
|
||||
{{ entry.description|desc2html }}
|
||||
|
||||
@@ -35,14 +35,13 @@
|
||||
{% endif %}
|
||||
|
||||
{% if documents|length > 0 %}
|
||||
{% embed '@theme/embeds/card.html.twig' with {'documents': documents} %}
|
||||
{% embed '@theme/embeds/card.html.twig' with {'documents': documents, fullsize: true} %}
|
||||
{% import "invoice/actions.html.twig" as actions %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_title %}{{ 'invoice_renderer'|trans({}, 'invoice-renderer') }}{% endblock %}
|
||||
{% block box_attributes %}
|
||||
id="invoice_document_list"
|
||||
{% endblock %}
|
||||
{% block box_body_class %}p-0{% endblock %}
|
||||
{% block box_body %}
|
||||
<table class="table table-hover dataTable">
|
||||
<thead>
|
||||
|
||||
@@ -90,14 +90,13 @@
|
||||
{% endif %}
|
||||
|
||||
{% if models|length > 0 %}
|
||||
{% embed '@theme/embeds/card.html.twig' %}
|
||||
{% embed '@theme/embeds/card.html.twig' with {fullsize: true} %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
{% block box_title %}
|
||||
{{ 'preview'|trans }}
|
||||
{% endblock %}
|
||||
{% block box_attributes %}id="invoice_customer_forms"{% endblock %}
|
||||
{% block box_body_class %}p-0{% endblock %}
|
||||
{% block box_body %}
|
||||
<table class="table dataTable">
|
||||
<thead>
|
||||
|
||||
@@ -3,40 +3,42 @@
|
||||
{% set metaField = entity.metaField(field.name) %}
|
||||
{% if not metaField is null %}
|
||||
{% set metaField = metaField.merge(field) %}
|
||||
{% set type = metaField.type %}
|
||||
{% set value = metaField.value %}
|
||||
{% if 'DurationType' in type %}
|
||||
{{ value|duration }}
|
||||
{% elseif 'YesNoType' in type or 'CheckboxType' in type %}
|
||||
{{ value ? ('yes'|trans) : ('no'|trans) }}
|
||||
{% elseif 'DatePickerType' in type %}
|
||||
{{ value|date_short }}
|
||||
{% elseif 'DateTimePickerType' in type %}
|
||||
{{ value|date_time }}
|
||||
{% elseif 'CountryType' in type %}
|
||||
{{ value|country_name }}
|
||||
{% elseif 'CurrencyType' in type %}
|
||||
{{ value|currency_name }}
|
||||
{% elseif 'LanguageType' in type %}
|
||||
{{ value|locale_name }}
|
||||
{% elseif 'MoneyType' in type %}
|
||||
{% set classname = class_name(entity) %}
|
||||
{% if classname == 'App\\Entity\\Timesheet' %}
|
||||
{{ value|money(entity.project.customer.currency) }}
|
||||
{% elseif classname == 'App\\Entity\\Customer' %}
|
||||
{{ value|money(entity.currency) }}
|
||||
{% elseif classname == 'App\\Entity\\Project' %}
|
||||
{{ value|money(entity.customer.currency) }}
|
||||
{% elseif classname == 'App\\Entity\\Activity' and entity.project is not null %}
|
||||
{{ value|money(entity.project.customer.currency) }}
|
||||
{% else %}
|
||||
{{ value }}
|
||||
{% endif %}
|
||||
{% elseif 'TextareaType' in type %}
|
||||
{{ value|nl2br }}
|
||||
{% else %}
|
||||
{# EmailType, UrlType, TagsType, ColorPickerType, ColorChoiceType #}
|
||||
{{ value }}
|
||||
{% endif %}
|
||||
{{ _self.form_type_value(metaField.type, metaField.value, entity) }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro form_type_value(type, value, entity) %}
|
||||
{% if 'DurationType' in type %}
|
||||
{{ value|duration }}
|
||||
{% elseif 'YesNoType' in type or 'CheckboxType' in type %}
|
||||
{{ value ? ('yes'|trans) : ('no'|trans) }}
|
||||
{% elseif 'DatePickerType' in type %}
|
||||
{{ value|date_short }}
|
||||
{% elseif 'DateTimePickerType' in type %}
|
||||
{{ value|date_time }}
|
||||
{% elseif 'CountryType' in type %}
|
||||
{{ value|country_name }}
|
||||
{% elseif 'CurrencyType' in type %}
|
||||
{{ value|currency_name }}
|
||||
{% elseif 'LanguageType' in type %}
|
||||
{{ value|locale_name }}
|
||||
{% elseif 'MoneyType' in type %}
|
||||
{% set classname = class_name(entity) %}
|
||||
{% if classname == 'App\\Entity\\Timesheet' %}
|
||||
{{ value|money(entity.project.customer.currency) }}
|
||||
{% elseif classname == 'App\\Entity\\Customer' %}
|
||||
{{ value|money(entity.currency) }}
|
||||
{% elseif classname == 'App\\Entity\\Project' %}
|
||||
{{ value|money(entity.customer.currency) }}
|
||||
{% elseif classname == 'App\\Entity\\Activity' and entity.project is not null %}
|
||||
{{ value|money(entity.project.customer.currency) }}
|
||||
{% else %}
|
||||
{{ value }}
|
||||
{% endif %}
|
||||
{% elseif 'TextareaType' in type %}
|
||||
{{ value|nl2br }}
|
||||
{% else %}
|
||||
{# EmailType, UrlType, TagsType, ColorPickerType, ColorChoiceType #}
|
||||
{{ value }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -7,9 +7,8 @@
|
||||
{% if plugins|length == 0 %}
|
||||
{{ widgets.callout('warning', 'plugin.none_installed'|trans({}, 'plugins')) }}
|
||||
{% else %}
|
||||
{% embed '@theme/embeds/card.html.twig' %}
|
||||
{% embed '@theme/embeds/card.html.twig' with {fullsize: true} %}
|
||||
{% block box_title %}{{ 'plugin.installed'|trans }}{% endblock %}
|
||||
{% block box_body_class %}p-0{% endblock %}
|
||||
{% block box_body %}
|
||||
{% set columns = {
|
||||
'name': {'class': 'alwaysVisible w-25'},
|
||||
@@ -46,10 +45,9 @@
|
||||
{% endif %}
|
||||
|
||||
{% if extensions|length > 0 %}
|
||||
{% embed '@theme/embeds/card.html.twig' %}
|
||||
{% embed '@theme/embeds/card.html.twig' with {fullsize: true} %}
|
||||
{% from "macros/widgets.html.twig" import card_tool_button %}
|
||||
{% block box_title %}{{ 'plugin.marketplace'|trans({}, 'plugins') }}{% endblock %}
|
||||
{% block box_body_class %}p-0{% endblock %}
|
||||
{% block box_tools %}
|
||||
{{ card_tool_button('shop', {'title': 'shop', 'translation_domain': 'plugins', 'target': '_blank', 'url': constant('App\\Constants::HOMEPAGE') ~ '/store/', 'icon': false, 'class': 'btn-primary'}) }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{% block main %}
|
||||
{% set can_edit = is_granted('edit', project) %}
|
||||
|
||||
{% embed '@theme/embeds/card.html.twig' %}
|
||||
{% embed '@theme/embeds/card.html.twig' with {fullsize: true} %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "customer/actions.html.twig" as customerActions %}
|
||||
{% block box_attributes %}id="project_details_box"{% endblock %}
|
||||
@@ -16,7 +16,6 @@
|
||||
{% block box_title %}
|
||||
{{ widgets.label_dot(project.name, project.color) }}
|
||||
{% endblock %}
|
||||
{% block box_body_class %}p-0{% endblock %}
|
||||
{% block box_body %}
|
||||
{% if project.comment is not empty %}
|
||||
<div class="comment p-3">
|
||||
|
||||
@@ -17,5 +17,8 @@
|
||||
{% block column_classes_total -%}
|
||||
{% if column is weekend %} weekend{% endif %}
|
||||
{%- endblock %}
|
||||
{% block column_classes_customer_total -%}
|
||||
{% if column is weekend %} weekend{% endif %}
|
||||
{%- endblock %}
|
||||
{% endembed %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -46,6 +46,23 @@
|
||||
{% elseif data.customer != totalCurrency %}
|
||||
{% set totalCurrency = null %}
|
||||
{% endif %}
|
||||
{% set customerTotalsDuration = {} %}
|
||||
{% set customerTotalsInternalRate = {} %}
|
||||
{% set customerTotalsRate = {} %}
|
||||
{% for column in period.dateTimes %}
|
||||
{% set dateKey = column|report_date %}
|
||||
{% set customerTotalsDuration = customerTotalsDuration|merge({(dateKey): 0}) %}
|
||||
{% set customerTotalsInternalRate = customerTotalsInternalRate|merge({(dateKey): 0}) %}
|
||||
{% set customerTotalsRate = customerTotalsRate|merge({(dateKey): 0}) %}
|
||||
{% endfor %}
|
||||
{% for project in data.projects %}
|
||||
{% for column in project.data.data %}
|
||||
{% set dateKey = column.date|report_date %}
|
||||
{% set customerTotalsDuration = customerTotalsDuration|merge({(dateKey): (customerTotalsDuration[dateKey] + column.duration)}) %}
|
||||
{% set customerTotalsInternalRate = customerTotalsInternalRate|merge({(dateKey): (customerTotalsInternalRate[dateKey] + column.internalRate)}) %}
|
||||
{% set customerTotalsRate = customerTotalsRate|merge({(dateKey): (customerTotalsRate[dateKey] + column.rate)}) %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<tr class="summary">
|
||||
<td>{{ widgets.label_customer(data.customer) }}</td>
|
||||
<td class="text-center total">
|
||||
@@ -59,7 +76,20 @@
|
||||
{% endif %}
|
||||
{% if with_links %}</a>{% endif %}
|
||||
</td>
|
||||
<td colspan="{{ columns }}"></td>
|
||||
{% for column in period.dateTimes %}
|
||||
{% set dateKey = column|report_date %}
|
||||
<td class="text-nowrap text-center day-total {% block column_classes_customer_total %}{% endblock %}">
|
||||
{% if customerTotalsRate[dateKey] > 0 or customerTotalsInternalRate[dateKey] > 0 or customerTotalsDuration[dateKey] > 0 %}
|
||||
{% if dataType == 'rate' %}
|
||||
<strong>{{ customerTotalsRate[dateKey]|money(currency) }}</strong>
|
||||
{% elseif dataType == 'internalRate' %}
|
||||
<strong>{{ customerTotalsInternalRate[dateKey]|money(currency) }}</strong>
|
||||
{% else %}
|
||||
<strong>{{ customerTotalsDuration[dateKey]|duration(decimal) }}</strong>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% for project in data.projects %}
|
||||
{% set absoluteDuration = absoluteDuration + project.duration %}
|
||||
|
||||
@@ -14,5 +14,6 @@
|
||||
{% block column_classes_project %}{% endblock %}
|
||||
{% block column_classes_activity %}{% endblock %}
|
||||
{% block column_classes_total %}{% endblock %}
|
||||
{% block column_classes_customer_total %}{% endblock %}
|
||||
{% endembed %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -5,4 +5,5 @@
|
||||
{% block column_classes_project %}{% endblock %}
|
||||
{% block column_classes_activity %}{% endblock %}
|
||||
{% block column_classes_total %}{% endblock %}
|
||||
{% block column_classes_customer_total %}{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
@@ -9,8 +9,11 @@
|
||||
|
||||
namespace App\Tests\API;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Invoice;
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Tests\DataFixtures\InvoiceFixtures;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
@@ -123,4 +126,59 @@ class InvoiceControllerTest extends APIControllerBaseTestCase
|
||||
{
|
||||
$this->assertEntityNotFound(User::ROLE_USER, '/api/invoices/' . PHP_INT_MAX);
|
||||
}
|
||||
|
||||
public function testDownloadRespectsCustomerPermission(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
|
||||
$invoices = $this->importInvoiceFixtures(1, [Invoice::STATUS_NEW]);
|
||||
$invoice = $invoices[0];
|
||||
$customer = $invoice->getCustomer();
|
||||
self::assertInstanceOf(Customer::class, $customer);
|
||||
|
||||
$this->assertAccessIsGranted($client, '/api/invoices/' . $invoice->getId());
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
self::assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
self::assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('Invoice', $result);
|
||||
|
||||
$team = new Team('foo');
|
||||
$team->addTeamlead($this->getUserByRole(User::ROLE_ADMIN));
|
||||
$team->addCustomer($customer);
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
/** @var TeamRepository $repository */
|
||||
$repository = $em->getRepository(Team::class);
|
||||
$repository->saveTeam($team);
|
||||
|
||||
$this->assertApiAccessDenied($client, '/api/invoices/' . $invoice->getId());
|
||||
}
|
||||
|
||||
public function testCollectionRespectsCustomerPermission(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
|
||||
$invoices = $this->importInvoiceFixtures(1, [Invoice::STATUS_NEW]);
|
||||
$invoice = $invoices[0];
|
||||
$customer = $invoice->getCustomer();
|
||||
self::assertInstanceOf(Customer::class, $customer);
|
||||
|
||||
$query = ['customers' => [$customer->getId()]];
|
||||
$this->assertAccessIsGranted($client, '/api/invoices', 'GET', $query);
|
||||
|
||||
$team = new Team('foo');
|
||||
$team->addTeamlead($this->getUserByRole(User::ROLE_ADMIN));
|
||||
$team->addCustomer($customer);
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
/** @var TeamRepository $repository */
|
||||
$repository = $em->getRepository(Team::class);
|
||||
$repository->saveTeam($team);
|
||||
|
||||
$this->request($client, '/api/invoices', 'GET', $query);
|
||||
$this->assertApiResponseAccessDenied($client->getResponse());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ class ExportControllerTest extends AbstractControllerBaseTestCase
|
||||
// assert export type buttons are available
|
||||
$expected = [
|
||||
'csv' => 'csv',
|
||||
'html' => 'html',
|
||||
'print' => 'html',
|
||||
'pdf' => 'pdf',
|
||||
'xlsx' => 'xlsx'
|
||||
];
|
||||
@@ -162,7 +162,7 @@ class ExportControllerTest extends AbstractControllerBaseTestCase
|
||||
// assert export type buttons are available
|
||||
$expected = [
|
||||
'csv' => 'csv',
|
||||
'html' => 'html',
|
||||
'print' => 'html',
|
||||
'pdf' => 'pdf',
|
||||
'xlsx' => 'xlsx'
|
||||
];
|
||||
@@ -227,7 +227,7 @@ class ExportControllerTest extends AbstractControllerBaseTestCase
|
||||
|
||||
// don't add daterange to make sure the current month is the default range
|
||||
$client->submit($form, [
|
||||
'renderer' => 'html',
|
||||
'renderer' => 'print',
|
||||
'markAsExported' => 1
|
||||
]);
|
||||
|
||||
|
||||
@@ -50,4 +50,23 @@ class WorkingTimeTest extends TestCase
|
||||
self::assertSame($approvedBy, $sut->getApprovedBy());
|
||||
self::assertTrue($sut->isApproved());
|
||||
}
|
||||
|
||||
public function testExpectedTime(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->setUsername('bar');
|
||||
$date = new \DateTimeImmutable();
|
||||
$sut = new WorkingTime($user, $date);
|
||||
$sut->setExpectedTime(222222);
|
||||
|
||||
self::assertEquals(222222, $sut->getExpectedTime());
|
||||
self::assertEquals(222222, $sut->getOriginalExpectedTime());
|
||||
$reducedBy = $sut->halveExpectedTime();
|
||||
self::assertEquals(111111, $reducedBy);
|
||||
self::assertEquals(111111, $sut->getExpectedTime());
|
||||
self::assertEquals(222222, $sut->getOriginalExpectedTime());
|
||||
$sut->emptyExpectedTime();
|
||||
self::assertEquals(0, $sut->getExpectedTime());
|
||||
self::assertEquals(222222, $sut->getOriginalExpectedTime());
|
||||
}
|
||||
}
|
||||
|
||||
31
tests/Event/TimesheetStatisticsQueryEventTest.php
Normal file
31
tests/Event/TimesheetStatisticsQueryEventTest.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Event;
|
||||
|
||||
use App\Event\TimesheetStatisticsQueryEvent;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(TimesheetStatisticsQueryEvent::class)]
|
||||
class TimesheetStatisticsQueryEventTest extends TestCase
|
||||
{
|
||||
public function testGetter(): void
|
||||
{
|
||||
$qb = new QueryBuilder($this->createMock(EntityManager::class));
|
||||
self::assertCount(0, $qb->getParameters());
|
||||
$sut = new TimesheetStatisticsQueryEvent($qb);
|
||||
$qb->setParameter('foo', 'bar');
|
||||
|
||||
self::assertSame($qb, $sut->getQueryBuilder());
|
||||
self::assertCount(1, $sut->getQueryBuilder()->getParameters());
|
||||
}
|
||||
}
|
||||
40
tests/Event/WorkingTimeQueryStatsEventTest.php
Normal file
40
tests/Event/WorkingTimeQueryStatsEventTest.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\WorkingTimeQueryStatsEvent;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(WorkingTimeQueryStatsEvent::class)]
|
||||
class WorkingTimeQueryStatsEventTest extends TestCase
|
||||
{
|
||||
public function testGetter(): void
|
||||
{
|
||||
$qb = new QueryBuilder($this->createMock(EntityManager::class));
|
||||
self::assertCount(0, $qb->getParameters());
|
||||
|
||||
$user = new User();
|
||||
$begin = new \DateTime('2004-02-13');
|
||||
$end = new \DateTime('2099-12-31');
|
||||
|
||||
$sut = new WorkingTimeQueryStatsEvent($qb, $user, $begin, $end);
|
||||
$qb->setParameter('foo', 'bar');
|
||||
|
||||
self::assertSame($qb, $sut->getQueryBuilder());
|
||||
self::assertCount(1, $sut->getQueryBuilder()->getParameters());
|
||||
self::assertSame($user, $sut->getUser());
|
||||
self::assertSame($begin, $sut->getBegin());
|
||||
self::assertSame($end, $sut->getEnd());
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ class XlsxRendererTest extends AbstractRendererTestCase
|
||||
|
||||
self::assertEquals('xlsx', $sut->getType());
|
||||
self::assertEquals('xlsx', $sut->getId());
|
||||
self::assertEquals('default', $sut->getTitle());
|
||||
self::assertEquals('xlsx', $sut->getTitle());
|
||||
self::assertFalse($sut->isInternal());
|
||||
$sut->setInternal(true);
|
||||
self::assertTrue($sut->isInternal());
|
||||
|
||||
@@ -15,13 +15,17 @@ use App\Form\Type\QuickEntryTimesheetType;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Form\FormExtensionInterface;
|
||||
use Symfony\Component\Form\PreloadedExtension;
|
||||
use Symfony\Component\Form\Test\TypeTestCase;
|
||||
|
||||
#[CoversClass(QuickEntryTimesheetType::class)]
|
||||
class QuickEntryTimesheetTypeTest extends TypeTestCase
|
||||
{
|
||||
protected function getExtensions()
|
||||
/**
|
||||
* @return FormExtensionInterface[]
|
||||
*/
|
||||
protected function getExtensions(): array
|
||||
{
|
||||
$auth = $this->createMock(Security::class);
|
||||
$auth->method('getUser')->willReturn(new User());
|
||||
@@ -36,18 +40,19 @@ class QuickEntryTimesheetTypeTest extends TypeTestCase
|
||||
|
||||
public static function getTestData()
|
||||
{
|
||||
yield [4.5, 16200];
|
||||
yield ['4,5', 16200];
|
||||
yield ['4:30', 16200];
|
||||
yield ['4h30m', 16200];
|
||||
yield [4.5, 0, 16200];
|
||||
yield ['4,5', 0, 16200];
|
||||
yield ['4:30', 0, 16200];
|
||||
yield ['4h30m', 0, 16200];
|
||||
yield ['4h30m', 1800, 16200]; // it is important, that the duration does not change with breaks
|
||||
}
|
||||
|
||||
#[DataProvider('getTestData')]
|
||||
public function testSubmitValidData($value, $expectedDuration): void
|
||||
public function testSubmitValidData(string|float $value, int $break, int $expectedDuration): void
|
||||
{
|
||||
$data = ['duration' => $value];
|
||||
|
||||
$model = $this->createDefaultModel();
|
||||
$model = $this->createDefaultModel($expectedDuration, $break);
|
||||
|
||||
$form = $this->factory->create(QuickEntryTimesheetType::class, $model);
|
||||
|
||||
@@ -56,16 +61,19 @@ class QuickEntryTimesheetTypeTest extends TypeTestCase
|
||||
self::assertTrue($form->isSynchronized());
|
||||
self::assertEquals($expectedDuration, $model->getDuration());
|
||||
self::assertEquals($expectedDuration, $model->getDuration(true));
|
||||
self::assertEquals($break, $model->getBreak());
|
||||
}
|
||||
|
||||
private function createDefaultModel(): Timesheet
|
||||
private function createDefaultModel(int $duration = 0, int $break = 0): Timesheet
|
||||
{
|
||||
$begin = new \DateTime('2020-02-15 12:30:00');
|
||||
$end = new \DateTime('2020-02-15 14:00:00');
|
||||
|
||||
$model = new Timesheet();
|
||||
$model->setBegin($begin);
|
||||
//$model->setDuration($duration);
|
||||
$model->setEnd($end);
|
||||
$model->setBreak($break);
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class MarkdownTest extends TestCase
|
||||
asdfasdfa</p>
|
||||
<pre><code>ssdfsdf</code></pre>
|
||||
<p><a href="http://example.com/foo-bar.html" target="_blank">http://example.com/foo-bar.html</a><br />
|
||||
<a href="file:///home/kimai/images/beautiful-flower.png" target="_blank">file:///home/kimai/images/beautiful-flower.png</a></p>
|
||||
file:///home/kimai/images/beautiful-flower.png</p>
|
||||
<p>sdfsdf <a href="#test-1">asdfasdf</a> asdfasdf</p>
|
||||
<p># test<br />
|
||||
aasdfasdf<br />
|
||||
|
||||
@@ -21,7 +21,7 @@ class ParsedownExtensionTest extends TestCase
|
||||
public function testTableContainsCssClasses(): void
|
||||
{
|
||||
$sut = new ParsedownExtension();
|
||||
$html = $sut->parse('
|
||||
$html = $sut->text('
|
||||
| Item | Price |
|
||||
|---|---|
|
||||
| Something | $ 472,78 |
|
||||
@@ -34,7 +34,7 @@ class ParsedownExtensionTest extends TestCase
|
||||
public function testHeaderIsNotConverted(): void
|
||||
{
|
||||
$sut = new ParsedownExtension();
|
||||
$html = $sut->parse('
|
||||
$html = $sut->text('
|
||||
# Foo
|
||||
');
|
||||
self::assertEquals('<p># Foo</p>', $html);
|
||||
|
||||
@@ -19,7 +19,7 @@ class ParsedownTest extends TestCase
|
||||
public function testTableContainsCssClasses(): void
|
||||
{
|
||||
$sut = new Parsedown();
|
||||
$html = $sut->parse('
|
||||
$html = $sut->text('
|
||||
| Item | Price |
|
||||
|---|---|
|
||||
| Something | $ 472,78 |
|
||||
@@ -32,7 +32,7 @@ class ParsedownTest extends TestCase
|
||||
public function testHeaderContainsId(): void
|
||||
{
|
||||
$sut = new Parsedown();
|
||||
$html = $sut->parse('
|
||||
$html = $sut->text('
|
||||
# Foo
|
||||
');
|
||||
self::assertEquals('<h1 id="foo">Foo</h1>', $html);
|
||||
@@ -41,7 +41,7 @@ class ParsedownTest extends TestCase
|
||||
public function testHeaderContainsIdDoesNotDuplicate(): void
|
||||
{
|
||||
$sut = new Parsedown();
|
||||
$html = $sut->parse('
|
||||
$html = $sut->text('
|
||||
# Foo
|
||||
# Foo
|
||||
# Foo
|
||||
|
||||
@@ -1346,26 +1346,11 @@ parameters:
|
||||
count: 1
|
||||
path: Form/Type/DurationTypeTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Form\\\\Type\\\\QuickEntryTimesheetTypeTest\\:\\:getExtensions\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Form/Type/QuickEntryTimesheetTypeTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Form\\\\Type\\\\QuickEntryTimesheetTypeTest\\:\\:getTestData\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Form/Type/QuickEntryTimesheetTypeTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Form\\\\Type\\\\QuickEntryTimesheetTypeTest\\:\\:testSubmitValidData\\(\\) has parameter \\$expectedDuration with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Form/Type/QuickEntryTimesheetTypeTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Form\\\\Type\\\\QuickEntryTimesheetTypeTest\\:\\:testSubmitValidData\\(\\) has parameter \\$value with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Form/Type/QuickEntryTimesheetTypeTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\Calculator\\\\AbstractCalculatorTestCase\\:\\:assertDescription\\(\\) has parameter \\$addActivity with no type specified\\.$#"
|
||||
count: 1
|
||||
|
||||
@@ -34,6 +34,18 @@
|
||||
<source>This year — until today</source>
|
||||
<target state="translated">Tahun ini — hingga hari ini</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8f61k.L" resname="daterangepicker.thisMonth" xml:space="preserve">
|
||||
<source>daterangepicker.thisMonth</source>
|
||||
<target state="translated">Bulan ini</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="AdEC6w4" resname="daterangepicker.lastMonth" xml:space="preserve">
|
||||
<source>daterangepicker.lastMonth</source>
|
||||
<target state="translated">Bulan lalu</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="mgJ2Z7M" resname="daterangepicker.thisFinancialYear" xml:space="preserve">
|
||||
<source>daterangepicker.thisFinancialYear</source>
|
||||
<target state="translated">Tahun fiskal ini</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
<source>daterangepicker.thisYear</source>
|
||||
<target state="translated">올해</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="AY_E9UR" resname="daterangepicker.thisYearUntilNow" xml:space="preserve">
|
||||
<source>This year — until today</source>
|
||||
<target state="translated">올해 — 오늘까지</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -42,6 +42,10 @@
|
||||
<source>daterangepicker.lastMonth</source>
|
||||
<target state="translated">Tháng trước</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="mgJ2Z7M" resname="daterangepicker.thisFinancialYear" xml:space="preserve">
|
||||
<source>daterangepicker.thisFinancialYear</source>
|
||||
<target state="translated">Năm tài chính hiện tại</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -34,6 +34,18 @@
|
||||
<source>daterangepicker.today</source>
|
||||
<target state="final">今天</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8f61k.L" resname="daterangepicker.thisMonth" xml:space="preserve">
|
||||
<source>daterangepicker.thisMonth</source>
|
||||
<target state="translated">本月份</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="AdEC6w4" resname="daterangepicker.lastMonth" xml:space="preserve">
|
||||
<source>daterangepicker.lastMonth</source>
|
||||
<target state="translated">上個月</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="mgJ2Z7M" resname="daterangepicker.thisFinancialYear" xml:space="preserve">
|
||||
<source>daterangepicker.thisFinancialYear</source>
|
||||
<target state="translated">本會計年度</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -38,6 +38,18 @@
|
||||
<source>Don't worry, Kimai will help you create a new one!</source>
|
||||
<target state="needs-translation">… επομένως ξεχάσατε τον κωδικό σας; Μην ανησυχείτε: Το Kimai θα σας βοηθήσει να δημιουργήσετε ένα νέο:</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="U.JjI9R" resname="approval_rejected_message" xml:space="preserve">
|
||||
<source>approval_rejected_message</source>
|
||||
<target state="translated">Η αίτηση εξουσιοδότησής σας απορρίφθηκε από τον/την %created_by%
|
||||
|
||||
%list%
|
||||
|
||||
Παρακαλούμε ελέγξτε τα αιτήματά σας στο:%url%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="YdGZyer" resname="approval_approved_subject" xml:space="preserve">
|
||||
<source>approval_approved_subject</source>
|
||||
<target state="translated">Το αίτημα σας έχει εγκριθεί</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="es" datatype="plaintext" original="email.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="8jDJmrK" resname="automated_email_dont_answer">
|
||||
<trans-unit id="8jDJmrK" resname="automated_email_dont_answer" xml:space="preserve">
|
||||
<source>automated_email_dont_answer</source>
|
||||
<target state="translated">Este es un correo electrónico generado automáticamente. Por favor, no responda, la dirección no será leída.</target>
|
||||
<target state="translated">Este es un correo-e auto-generado. Por favor, no responda, la dirección no será leída.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="pjr3FDv" resname="registration.subject">
|
||||
<trans-unit id="pjr3FDv" resname="registration.subject" xml:space="preserve">
|
||||
<source>registration.subject</source>
|
||||
<target state="translated">activa tu cuenta</target>
|
||||
<target state="translated">Activa tu cuenta</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="QtcznHC" resname="registration.title">
|
||||
<source>registration.title</source>
|
||||
@@ -38,9 +38,9 @@
|
||||
<source>Don't worry, Kimai will help you create a new one!</source>
|
||||
<target state="translated">Este enlace solo será válido durante los próximos 15 minutos.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="yohIAA_" resname="absence_created_supervisor_subject">
|
||||
<trans-unit id="yohIAA_" resname="absence_created_supervisor_subject" xml:space="preserve">
|
||||
<source>absence_created_supervisor_subject</source>
|
||||
<target state="translated">Nueva ausencia | %user% | %type%</target>
|
||||
<target state="translated">Nueva ausencia: %user% (%type%)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="wwLx._J" resname="absence_created_supervisor_message" xml:space="preserve">
|
||||
<source>A new absence was created for %user%.
|
||||
@@ -48,7 +48,7 @@
|
||||
%list%
|
||||
|
||||
Please review them at: %url%</source>
|
||||
<target state="translated">Se ha creado una nueva ausencia para %user%.
|
||||
<target state="translated">Se ha creado una ausencia nueva para %user%.
|
||||
|
||||
%list%
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
|
||||
<source>reset.button</source>
|
||||
<target state="translated">Connectez-vous</target>
|
||||
<target state="translated">Se connecter</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
|
||||
<source>Don't worry, Kimai will help you create a new one!</source>
|
||||
|
||||
@@ -50,9 +50,9 @@ Please review them at: %url%</source>
|
||||
|
||||
Kérjük, tekintse át a következő címen: %url%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="yohIAA_" resname="absence_created_supervisor_subject">
|
||||
<trans-unit id="yohIAA_" resname="absence_created_supervisor_subject" xml:space="preserve">
|
||||
<source>absence_created_supervisor_subject</source>
|
||||
<target state="translated">Új távollét | %user% | %type%</target>
|
||||
<target state="translated">Új távollét: %user% (%type%)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="1q.VHdU" resname="approval_requested_subject" xml:space="preserve">
|
||||
<source>approval_requested_subject</source>
|
||||
|
||||
@@ -32,11 +32,11 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
|
||||
<source>reset.button</source>
|
||||
<target state="needs-translation">Zresetuj swoje hasło</target>
|
||||
<target state="translated">Zaloguj się</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
|
||||
<source>Don't worry, Kimai will help you create a new one!</source>
|
||||
<target state="needs-translation">… zapomniałeś hasła? Nie martw się, Kimai pomoże Ci stworzyć nowe:</target>
|
||||
<target state="translated">Link będzie ważny przez 15 najbliższych minut</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="yohIAA_" resname="absence_created_supervisor_subject" xml:space="preserve">
|
||||
<source>absence_created_supervisor_subject</source>
|
||||
@@ -54,6 +54,42 @@ Please review them at: %url%</source>
|
||||
|
||||
Prosimy o zapoznanie się z nią na stronie: %url%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="1q.VHdU" resname="approval_requested_subject" xml:space="preserve">
|
||||
<source>approval_requested_subject</source>
|
||||
<target state="translated">Wniosek o zatwierdzenie: %user%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Z172mQ8" resname="approval_requested_message" xml:space="preserve">
|
||||
<source>approval_requested_message</source>
|
||||
<target state="translated">Wysłano prośbę o zatwierdzenie dla %user%.
|
||||
|
||||
%list%
|
||||
|
||||
Proszę zapoznać się z nimi pod adresem: %url%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="dvPkiia" resname="approval_rejected_subject" xml:space="preserve">
|
||||
<source>approval_rejected_subject</source>
|
||||
<target state="translated">Twoja prośba została odrzucona</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="U.JjI9R" resname="approval_rejected_message" xml:space="preserve">
|
||||
<source>approval_rejected_message</source>
|
||||
<target state="translated">Twoja prośba o autoryzację została odrzucona przez %created_by%.
|
||||
|
||||
%list%
|
||||
|
||||
Proszę sprawdzić swoje wnioski pod adresem: %url%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="YdGZyer" resname="approval_approved_subject" xml:space="preserve">
|
||||
<source>approval_approved_subject</source>
|
||||
<target state="translated">Twój wniosek został zaakceptowany</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="4nn7Vi1" resname="approval_approved_message" xml:space="preserve">
|
||||
<source>approval_approved_message</source>
|
||||
<target state="translated">Twój wniosek został zaakceptowany przez %created_by%.
|
||||
|
||||
%list%
|
||||
|
||||
Wszystkie zapytania można znaleźć pod adresem: %url%</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -54,6 +54,42 @@ Please review them at: %url%</source>
|
||||
|
||||
Vui lòng xét duyệt tại: %url%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="1q.VHdU" resname="approval_requested_subject" xml:space="preserve">
|
||||
<source>approval_requested_subject</source>
|
||||
<target state="translated">Quyền cấp phép đã được yêu cầu:%user%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Z172mQ8" resname="approval_requested_message" xml:space="preserve">
|
||||
<source>approval_requested_message</source>
|
||||
<target state="translated">Quyền cấp phép cho %user% đã được yêu cầu
|
||||
|
||||
%list%
|
||||
|
||||
Hãy xem qua tại:%url%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="dvPkiia" resname="approval_rejected_subject" xml:space="preserve">
|
||||
<source>approval_rejected_subject</source>
|
||||
<target state="translated">Yêu cầu của bạn đã bị từ chối</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="U.JjI9R" resname="approval_rejected_message" xml:space="preserve">
|
||||
<source>approval_rejected_message</source>
|
||||
<target state="translated">Yêu cầu xác thực của bạn đã bị từ chối bởi %created_by%
|
||||
|
||||
%list%
|
||||
|
||||
Vui lòng xem qua yêu cầu của bạn tại: %url%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="YdGZyer" resname="approval_approved_subject" xml:space="preserve">
|
||||
<source>approval_approved_subject</source>
|
||||
<target state="translated">Yêu cầu của bạn đã được chấp nhận</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="4nn7Vi1" resname="approval_approved_message" xml:space="preserve">
|
||||
<source>approval_approved_message</source>
|
||||
<target state="translated">Yêu cầu của bạn đã được chấp nhận bởi %created_by%
|
||||
|
||||
%list%
|
||||
|
||||
Bạn có thể tất cả truy vấn của bạn tại: %url%</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -54,6 +54,42 @@ Please review them at: %url%</source>
|
||||
|
||||
請在此審核:%url%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="1q.VHdU" resname="approval_requested_subject" xml:space="preserve">
|
||||
<source>approval_requested_subject</source>
|
||||
<target state="translated">待核准: %user%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Z172mQ8" resname="approval_requested_message" xml:space="preserve">
|
||||
<source>approval_requested_message</source>
|
||||
<target state="translated">已為 %user% 提交審核請求。
|
||||
|
||||
%list%
|
||||
|
||||
請至以下連結審閱:%url%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="dvPkiia" resname="approval_rejected_subject" xml:space="preserve">
|
||||
<source>approval_rejected_subject</source>
|
||||
<target state="translated">您的請求已被拒絕</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="YdGZyer" resname="approval_approved_subject" xml:space="preserve">
|
||||
<source>approval_approved_subject</source>
|
||||
<target state="translated">您的請求已獲批准</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="4nn7Vi1" resname="approval_approved_message" xml:space="preserve">
|
||||
<source>approval_approved_message</source>
|
||||
<target state="translated">您的請求已獲 %created_by% 核准。
|
||||
|
||||
%list%
|
||||
|
||||
您可於以下連結查閱所有相關紀錄:%url%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="U.JjI9R" resname="approval_rejected_message" xml:space="preserve">
|
||||
<source>approval_rejected_message</source>
|
||||
<target state="translated">您的授權請求已被 %created_by% 拒絕。
|
||||
|
||||
%list%
|
||||
|
||||
請至以下連結檢視您的請求:%url%</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="ar" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="dzZWfU1" resname="timesheet.pdf.twig">
|
||||
<source>Timesheet</source>
|
||||
<target>ورقة التوقيت</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="bg" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="dzZWfU1" resname="timesheet.pdf.twig" xml:space="preserve">
|
||||
<source>Timesheet</source>
|
||||
<target state="translated">Работно време</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="ca" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="dzZWfU1" resname="timesheet.pdf.twig" xml:space="preserve">
|
||||
<source>Timesheet</source>
|
||||
<target state="translated">Registre d'hores</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="cs" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="dzZWfU1" resname="timesheet.pdf.twig">
|
||||
<source>Timesheet</source>
|
||||
<target state="translated">Pracovní výkaz</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="da" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="dzZWfU1" resname="timesheet.pdf.twig" xml:space="preserve">
|
||||
<source>Timesheet</source>
|
||||
<target state="translated">Timeoverblik</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="de" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="dzZWfU1" resname="timesheet.pdf.twig">
|
||||
<source>Timesheet</source>
|
||||
<target>Stundennachweis</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="de-CH" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="dzZWfU1" resname="timesheet.pdf.twig">
|
||||
<source>Timesheet</source>
|
||||
<target state="translated">Stundenzettel</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="el" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="dzZWfU1" resname="timesheet.pdf.twig">
|
||||
<source>Timesheet</source>
|
||||
<target>Χρονοδιάγραμμα</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="en" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="dzZWfU1" resname="timesheet.pdf.twig">
|
||||
<source>Timesheet</source>
|
||||
<target>Timesheet</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="eo" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="es" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="dzZWfU1" resname="timesheet.pdf.twig">
|
||||
<source>Timesheet</source>
|
||||
<target state="translated">Parte de horas</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="et" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="eu" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="fa" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="dzZWfU1" resname="timesheet.pdf.twig">
|
||||
<source>Timesheet</source>
|
||||
<target state="translated">تایم شیت</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="fi" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="dzZWfU1" resname="timesheet.pdf.twig">
|
||||
<source>Timesheet</source>
|
||||
<target state="translated">Kellokortti</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="fo" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="fr" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="dzZWfU1" resname="timesheet.pdf.twig">
|
||||
<source>Timesheet</source>
|
||||
<target>Feuille de temps</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="he" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="dzZWfU1" resname="timesheet.pdf.twig">
|
||||
<source>Timesheet</source>
|
||||
<target>גיליון שעות</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="hr" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="dzZWfU1" resname="timesheet.pdf.twig" xml:space="preserve">
|
||||
<source>Timesheet</source>
|
||||
<target state="translated">Vremenska tablica</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="hu" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="dzZWfU1" resname="timesheet.pdf.twig" xml:space="preserve">
|
||||
<source>Timesheet</source>
|
||||
<target state="translated">Munkaidő-nyilvántartás</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user