Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2033: Sync worklogs from invoice entry #147

Merged
merged 43 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
d8e8460
Passing invoice id to invoice entry worklog form class
jeppekroghitk Aug 8, 2024
40376b2
Prepared form class to recieve invoice id, and getting periodFrom and…
jeppekroghitk Aug 8, 2024
4886af4
Updated changelog
jeppekroghitk Aug 8, 2024
8131b98
added button to template, created stylings, js controller and got js …
jeppekroghitk Aug 8, 2024
9c45c69
Added logic to worklog sync method
jeppekroghitk Aug 8, 2024
17e43c1
Added styling and improved javascript handling button
jeppekroghitk Aug 8, 2024
5352ac2
Updated changelog
jeppekroghitk Aug 8, 2024
f6f355e
Added page reload post sync success
jeppekroghitk Aug 8, 2024
e70d1a5
Modified page-header component to utilize upcoming action-button comp…
jeppekroghitk Aug 9, 2024
c4e9667
Added action button twig component
jeppekroghitk Aug 9, 2024
78c7506
Added js controller for action-button and attached postrequesthandler
jeppekroghitk Aug 9, 2024
d4013d9
Added icons to icon twig component
jeppekroghitk Aug 9, 2024
75130b8
Implemented new action button for worklog entry page header
jeppekroghitk Aug 9, 2024
1d56113
Converted some inline css to tailwind and added rest to app.css
jeppekroghitk Aug 12, 2024
9a3e8b8
Cleaned up
jeppekroghitk Aug 12, 2024
d833bbf
Final adjustments
jeppekroghitk Aug 12, 2024
9e42e47
coding standards
jeppekroghitk Aug 12, 2024
1bab0ba
Fixed issue where isBillable on project could not be unset
jeppekroghitk Aug 8, 2024
cc8592e
Fixed typo in isBillable form label, added missing translations and m…
jeppekroghitk Aug 8, 2024
b71e820
Added included=true to project filters by default
jeppekroghitk Aug 8, 2024
2143fa1
Added initial project get method to only include included projects, i…
jeppekroghitk Aug 8, 2024
6f08ea2
Changed incorrect definition of false when settings isBillable to false
jeppekroghitk Aug 8, 2024
f2b6a95
Updated changelog
jeppekroghitk Aug 8, 2024
3afdcc0
Passing periodFrom and periodTo to filtertype instead of invoiceid to…
jeppekroghitk Aug 12, 2024
8ece8c7
Set default value to projectFilterData instead of settings it alterna…
jeppekroghitk Aug 12, 2024
3c086ab
coding standards
jeppekroghitk Aug 12, 2024
c8176d2
Passing invoice id to invoice entry worklog form class
jeppekroghitk Aug 8, 2024
07bb10b
Prepared form class to recieve invoice id, and getting periodFrom and…
jeppekroghitk Aug 8, 2024
d46cd1e
Updated changelog
jeppekroghitk Aug 8, 2024
084d9da
Revert "Prepared form class to recieve invoice id, and getting period…
jeppekroghitk Aug 13, 2024
db278dd
Revert "Updated changelog"
jeppekroghitk Aug 13, 2024
a317150
Reapply "Prepared form class to recieve invoice id, and getting perio…
jeppekroghitk Aug 13, 2024
1308016
Restored mistakenly overwritten force push
jeppekroghitk Aug 13, 2024
8e85780
Removed unused variable definition and constructor
jeppekroghitk Aug 13, 2024
d750df2
Added type link to existing headers with link button
jeppekroghitk Aug 13, 2024
2283422
Reapplied translation to worklog sync button
jeppekroghitk Aug 13, 2024
bcbde8c
Merge branch 'feature/2034-invoice-date-select-continuity' into featu…
jeppekroghitk Aug 13, 2024
0a3a998
Coding standards
jeppekroghitk Aug 13, 2024
723e2bf
Merge branch 'feature/2034-invoice-date-select-continuity' into featu…
jeppekroghitk Aug 13, 2024
ae75492
Restored original headline to Invoice worklog entry
jeppekroghitk Aug 13, 2024
a0daf44
Improved invoice worklog entry bottom sticky button bar
jeppekroghitk Aug 13, 2024
1c047cc
Throwing error on management-report when no invoice is found
jeppekroghitk Aug 13, 2024
f4e75ef
Fixed issue regarding translations for workload report
jeppekroghitk Aug 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

* [PR-147](https://github.com/itk-dev/economics/pull/147)
2033: Sync worklogs from invoice entry.
* [PR-146](https://github.com/itk-dev/economics/pull/146)
2034: Invoice date select continuity.
* [PR-145](https://github.com/itk-dev/economics/pull/145)
2059: Specify workload report week definition.
* [PR-143](https://github.com/itk-dev/economics/pull/143)
Expand Down
88 changes: 88 additions & 0 deletions assets/controllers/action-button_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Controller } from "@hotwired/stimulus";
import { postRequestHandler } from "../helpers/postRequestHandler";

const states = {
default: "default",
loading: "loading",
success: "success",
error: "error"
};

export default class extends Controller {

static targets = ["actionDefault", "actionLoading", "actionSuccess", "actionError"];

action(e) {
const targets = {
default: this.actionDefaultTarget,
loading: this.actionLoadingTarget,
success: this.actionSuccessTarget,
error: this.actionErrorTarget,
parent: e.target,
};
const data = e.target.dataset;
const url = data.url;
const reload = data.reload;

triggerState(states.loading, targets);
postRequestHandler(url)
.then(result => {
if (result.success) {
triggerState(states.success, targets);
setTimeout(()=>{
reload && window.location.reload();
triggerState(states.default, targets);
}, 2000)
} else {
triggerState(states.error, targets);
alert(result.error);
}
});
}
}

/**
* Applies a state to the provided targets based on the given state value.
*
* @param {string} state - The state value to apply.
* @param {object} targets - The targets to apply the state to.
*/
function triggerState(state, targets) {
resetState(targets);

switch (state) {
case states.default:
targets.default.classList.remove('hidden');
break;
case states.loading:
targets.loading.classList.remove("hidden");
break;
case states.success:
targets.success.classList.remove("hidden");
targets.parent.classList.add('btn-success');
break;
case states.error:
targets.error.classList.remove("hidden");
targets.parent.classList.add('btn-error');
break;
default:
console.log("State not recognized");
}
}

/**
* Resets the state of the targets.
*
* @param {object} targets - The targets to reset.
* @return {void}
*/
function resetState(targets)
{
Object.entries(targets).forEach(([type, target]) => {
target.classList.remove('btn-error', 'btn-success');
if (type === "parent") {
return;
}
target.classList.add('hidden');
});
}
35 changes: 35 additions & 0 deletions assets/helpers/postRequestHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export const postRequestHandler = async (updateUrl) => {
let result = {
success: false,
status: null,
data: null,
error: null
};

try {
const response = await fetch(updateUrl, {
method: "POST",
mode: "same-origin",
cache: "no-cache",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
redirect: "follow",
referrerPolicy: "no-referrer"
});

result.status = response.status;

if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message);
} else {
result.success = true;
result.data = await response.json();
}
} catch (error) {
console.error(error.message);
result.error = error.message;
}

return result;
};
13 changes: 13 additions & 0 deletions assets/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,19 @@
.worklogs-filter-form {
@apply grid gap-3 mb-6 md:grid-cols-4;
}
.action-button > span:not(.hidden) > svg {
margin: auto;
animation: spin 2s linear infinite
}

@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

.choices__list--dropdown .choices__item--selectable {
padding: 1em !important;
Expand Down
4 changes: 3 additions & 1 deletion src/Controller/InvoiceEntryWorklogController.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ public function worklogs(Request $request, Invoice $invoice, InvoiceEntry $invoi
}

$filterData = new InvoiceEntryWorklogsFilterData();
$form = $this->createForm(InvoiceEntryWorklogFilterType::class, $filterData);
$form = $this->createForm(InvoiceEntryWorklogFilterType::class, $filterData, [
'invoiceId' => $invoice->getId(),
]);

$form->add('version', EntityType::class, [
'class' => Version::class,
Expand Down
10 changes: 10 additions & 0 deletions src/Form/InvoiceEntryWorklogFilterType.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Form;

use App\Model\Invoices\InvoiceEntryWorklogsFilterData;
use App\Repository\InvoiceRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
Expand All @@ -11,8 +12,14 @@

class InvoiceEntryWorklogFilterType extends AbstractType
{
public function __construct(
private readonly InvoiceRepository $invoiceRepository,
) {
}

public function buildForm(FormBuilderInterface $builder, array $options): void
{
$invoice = $this->invoiceRepository->find($options['invoiceId'] ?? null);
$builder
->add('isBilled', ChoiceType::class, [
'required' => false,
Expand All @@ -31,6 +38,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'label_attr' => ['class' => 'label'],
'row_attr' => ['class' => 'form-row'],
'help' => 'worklog.period_from_helptext',
'data' => $invoice?->getPeriodFrom(),
'widget' => 'single_text',
'html5' => true,
'attr' => ['class' => 'form-element'],
Expand All @@ -42,6 +50,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'row_attr' => ['class' => 'form-row'],
'attr' => ['class' => 'form-element'],
'help' => 'worklog.period_to_helptext',
'data' => $invoice?->getPeriodTo(),
'widget' => 'single_text',
'html5' => true,
])
Expand Down Expand Up @@ -72,6 +81,7 @@ public function configureOptions(OptionsResolver $resolver): void
$resolver->setDefaults([
'method' => 'GET',
'data_class' => InvoiceEntryWorklogsFilterData::class,
'invoiceId' => null,
]);
}
}
32 changes: 32 additions & 0 deletions templates/components/action-button.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{#
This Twig template represents a stylized button component with different states that could be managed by a StumilusJS Controller.

Parameters:
- url (string, optional):
An URL which can be accessed when the button is clicked. Defaults to an empty string.
- reload (boolean, optional):
If set to true, triggers a page reload when the button is clicked. Defaults to false.
- default_text (string, optional):
The default text displayed on the button. Defaults to 'default'.
- loader_text (string, optional):
The text displayed when the button is in the state of loading. Should be defined together with link_text variable.
- success_text (string, optional):
The text displayed when an action related to the button is successfully finished. Defaults to 'success'.
- error_text (string, optional):
The text displayed when an action related to the button has failed. Defaults to 'error'.
#}

<button class="action-button button w-48 h-16" data-url="{{ url|default("") }}" data-reload="{{ reload|default(false) }}" {{ stimulus_controller('action-button') }} {{ stimulus_action('action-button', 'action') }}>
<span
class="action-default pointer-events-none"{{ stimulus_target('action-button', 'actionDefault') }}> {{ default_text|default('default') }}
</span>
<span
class="action-loading hidden pointer-events-none" {{ stimulus_target('action-button', 'actionLoading') }}> {{ loader_text is defined ? loader_text : include('components/icons.html.twig', {icon: 'spinner', class: 'w-6 h-6'}) }}
</span>
<span
class="action-success hidden pointer-events-none" {{ stimulus_target('action-button', 'actionSuccess') }}>{{ success_text|default('success') }}
</span>
<span
class="action-error hidden pointer-events-none" {{ stimulus_target('action-button', 'actionError') }}>{{ error_text|default('error') }}
</span>
</button>
4 changes: 4 additions & 0 deletions templates/components/icons.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />
</svg>
{% elseif icon == 'spinner' %}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="{{ class }}">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
{% endif %}
10 changes: 8 additions & 2 deletions templates/components/page-header.html.twig
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<header class="w-full flex justify-between items-center">
<h1 class="page-title">{{ title }}</h1>
{% if link_url|default(false) %}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change requires that you change the other parts that use:
components/page-header.html.twig
otherwise, they will not display the link

<a class="link border border-blue-600 rounded px-3 py-2 no-underline hover:bg-blue-50" href="{{ path(link_url) }}">{{ link_text }}</a>
{% if type is defined %}
{% if type == 'link' %}
<a class="link border border-blue-600 rounded px-3 py-2 no-underline hover:bg-blue-50" href="{{ path(link_url) }}">{{ link_text }}</a>
{% endif %}
{% if type == 'action' %}
{% include 'components/action-button.html.twig' %}
{% endif %}
{% endif %}

</header>
10 changes: 9 additions & 1 deletion templates/invoice_entry/worklogs.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@
{% block title %}{{ 'worklog.title'|trans }}{% endblock %}

{% block content %}
<h1 class="page-title">{{ 'worklog.title'|trans }}</h1>
{% include 'components/page-header.html.twig' with {
'title': 'invoices.title'|trans,
'type': 'action',
'default_text': 'fest',
'success_text': 'worklog.sync_worklogs_success'|trans,
'url': path('app_project_sync', {'id': invoice.project.id}),
'reload': true,
} %}

{{ form_start(form) }}
<div class="worklogs-filter-form selections" {{ stimulus_controller('choices') }}>
{{ form_rest(form) }}
<button class="button m-5">{{ 'invoices.search'|trans }}</button>
</div>

{{ form_end(form) }}

<div {{ stimulus_controller('entry-select') }} data-submit-endpoint="{{ submitEndpoint }}">
Expand Down
2 changes: 2 additions & 0 deletions translations/messages.da.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,8 @@ worklog:
only_available: "Kun \"frie\" worklogs"
only_available_helptext: "Worklogs der ikke er i en anden fakturalinje."
title_show: "Worklogs"
sync_worklogs: "Synkroniser worklogs"
sync_worklogs_success: "Succes - siden opdateres.."

invoice_entry:
amount: "Antal enheder"
Expand Down
Loading