diff --git a/AGENTS.md b/AGENTS.md index 2a522c4..739f783 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,8 @@ Bygg ett webbaserat system för hantering av utlägg (”claims”) åt en organ 8. Tillåt val av valuta per claimrad (default SEK) men håll valet dolt/avancerat för enklare UX. 9. Tillhandahåll en intern vy som låter användare med rätt behörighet skapa/uppdatera/ta bort konton och toggla `claims.view_claim`/`claims.change_claim`. 10. Claims ska kopplas till ett projekt/evenemang; projekten hanteras via Django admin. +11. Offentliga sidor ska använda Tailwind-baserade komponenter (CDN är okej) med minimalistisk layout. Claim-formuläret ska erbjuda klient-side kontroll för antal rader (plus/minus) utan sidladdning och återanvända formsetets tomma form som mall. +12. Adminvyn för claims ska spegla samma designprinciper (kort per claim, statuschippar, loggtimeline och inlinebeslut). ## Säkerhet och drift - Skydda admin-flöden bakom inloggning. diff --git a/claims/forms.py b/claims/forms.py index a668a70..d1fff02 100644 --- a/claims/forms.py +++ b/claims/forms.py @@ -6,11 +6,27 @@ from .models import Claim, Project User = get_user_model() +INPUT_CLASSES = "mt-1 block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500" +TEXTAREA_CLASSES = "mt-1 block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500" +SELECT_CLASSES = "mt-1 block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500" +FILE_CLASSES = "mt-1 block w-full text-sm text-gray-700 file:mr-4 file:rounded-md file:border-0 file:bg-indigo-600 file:px-4 file:py-2 file:text-sm file:font-medium file:text-white hover:file:bg-indigo-500" + class ClaimantForm(forms.Form): - full_name = forms.CharField(max_length=255, label="Namn") - email = forms.EmailField(label="E-post") - account_number = forms.CharField(max_length=50, label="Kontonummer") + full_name = forms.CharField( + max_length=255, + label="Namn", + widget=forms.TextInput(attrs={"class": INPUT_CLASSES}), + ) + email = forms.EmailField( + label="E-post", + widget=forms.EmailInput(attrs={"class": INPUT_CLASSES}), + ) + account_number = forms.CharField( + max_length=50, + label="Kontonummer", + widget=forms.TextInput(attrs={"class": INPUT_CLASSES}), + ) class ClaimLineForm(forms.ModelForm): @@ -20,6 +36,10 @@ class ClaimLineForm(forms.ModelForm): self.fields["currency"].initial = Claim.Currency.SEK self.fields["project"].queryset = Project.objects.filter(is_active=True).order_by("name") self.fields["project"].required = False + self.fields["project"].widget.attrs.update({"class": SELECT_CLASSES}) + self.fields["currency"].widget.attrs.update({"class": SELECT_CLASSES}) + self.fields["amount"].widget.attrs.update({"class": INPUT_CLASSES}) + self.fields["receipt"].widget.attrs.update({"class": FILE_CLASSES}) class Meta: model = Claim @@ -32,7 +52,7 @@ class ClaimLineForm(forms.ModelForm): "receipt": "Kvitto", } widgets = { - "description": forms.Textarea(attrs={"rows": 3}), + "description": forms.Textarea(attrs={"rows": 3, "class": TEXTAREA_CLASSES}), } diff --git a/claims/templates/claims/admin_list.html b/claims/templates/claims/admin_list.html index 955329a..3beb0b1 100644 --- a/claims/templates/claims/admin_list.html +++ b/claims/templates/claims/admin_list.html @@ -3,111 +3,142 @@ {% block title %}Admin – Utlägg{% endblock %} {% block content %} -

Inkomna utlägg

-

Endast användare med behörighet att se utlägg kommer åt den här sidan.

- -
- Filtrera: - Alla - {% for value, label in status_choices %} - | - - {{ label }} +
+
+
+

Översikt

+

Inkomna utlägg

+

Filtrera på status, granska kvitton och uppdatera beslut direkt i listan.

+
+
+ {% for value, label in status_choices %} + + {{ label }} + + {% endfor %} +
+ - - - - - - - - - - - {% if can_change %}{% endif %} - - - - {% for claim in claims %} - - - - - - - - - {% if can_change %} - - {% endif %} - - {% empty %} - - {% endfor %} - -
Person & kontaktBeloppProjektStatusKvittensLoggSenast uppdateradÅtgärd
- {{ claim.full_name }}
- {{ claim.email }}
- Konto: {{ claim.account_number }}
- {{ claim.description|linebreaksbr }} -
- {% if claim.submitted_by %} - Inskickad av inloggad användare: {{ claim.submitted_by.get_username }} - {% else %} - Inskickad av gäst - {% endif %} + {% if claims %} +
+ {% for claim in claims %} +
+
+
+
+ + {{ claim.amount }} {{ claim.currency }} + + {% if claim.project %} + + {{ claim.project }} + + {% endif %} + Skapad {{ claim.created_at|date:"Y-m-d H:i" }} +
+

{{ claim.full_name }}

+

+ {{ claim.email }} · Konto: {{ claim.account_number }}
+ {% if claim.submitted_by %} + Inloggad användare: {{ claim.submitted_by.get_username }} + {% else %} + Inskickad av gäst + {% endif %} +

+
+
+ + {{ claim.get_status_display }} + + {% if claim.decision_note %} +

Kommentar: {{ claim.decision_note }}

+ {% endif %} +
-
{{ claim.amount }} {{ claim.currency }}{{ claim.project|default:"-" }} - {{ claim.get_status_display }}
- {% if claim.decision_note %}Kommentar: {{ claim.decision_note }}{% endif %} -
- {% if claim.receipt %} - Visa fil - {% else %} - – - {% endif %} - -
- Visa logg -
    - {% for log in claim.logs.all %} -
  • - {{ log.created_at|date:"Y-m-d H:i" }} – - {{ log.get_action_display }}: - {% if log.from_status %}{{ log.get_from_status_display }} → {% endif %} - {{ log.get_to_status_display }} - {% if log.performed_by %} - (av {{ log.performed_by.get_username }}) - {% endif %} - {% if log.note %} - – "{{ log.note }}" - {% endif %} -
  • - {% empty %} -
  • Ingen logg än.
  • - {% endfor %} -
-
-
{{ claim.updated_at|date:"Y-m-d H:i" }} -
- {% csrf_token %} - - - - -
-
Inga utlägg än.
+ + + + {% if can_change %} +
+ {% csrf_token %} + + + + + + + + + +
+ {% endif %} + + + + {% endfor %} + + {% else %} +
+

Inga utlägg ännu

+

När formuläret tas emot visas posterna automatiskt här.

+
+ {% endif %} + {% endblock %} diff --git a/claims/templates/claims/base.html b/claims/templates/claims/base.html index b2dbfb0..ddb74d6 100644 --- a/claims/templates/claims/base.html +++ b/claims/templates/claims/base.html @@ -1,48 +1,69 @@ - + {% block title %}Claims{% endblock %} - + + - - -
- {% if messages %} - - {% endif %} - {% block content %}{% endblock %} + {% block content %}{% endblock %} + diff --git a/claims/templates/claims/includes/claim_formset.html b/claims/templates/claims/includes/claim_formset.html new file mode 100644 index 0000000..243b1fe --- /dev/null +++ b/claims/templates/claims/includes/claim_formset.html @@ -0,0 +1,168 @@ +{{ formset.management_form }} + +
+
+
+

Steg 2

+

Utläggsrader

+

Lägg till ett block per kvitto eller kostnad. Projektväljaren hjälper ekonomin att bokföra rätt.

+
+
+ + Totalt {{ current_forms }} rader + +
+ +
justera
+ +
+
+
+ + {% for form in formset %} +
+
+

Utlägg {{ forloop.counter }}

+

Obligatoriska fält markeras med *

+
+
+ {{ form.non_field_errors }} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + +
+ + {{ form.description }} + {% for error in form.description.errors %} +

{{ error }}

+ {% endfor %} +
+ +
+
+ + {{ form.amount }} + {% for error in form.amount.errors %} +

{{ error }}

+ {% endfor %} +
+
+ + {{ form.project }} + {% for error in form.project.errors %} +

{{ error }}

+ {% endfor %} +
+
+ +
+ + Avancerat: justera valuta (standard SEK) + +
+
+ + {{ form.currency }} + {% for error in form.currency.errors %} +

{{ error }}

+ {% endfor %} +
+

Använd detta om kvittot är i annan valuta än svenska kronor.

+
+
+ +
+ + {{ form.receipt }} + {% for error in form.receipt.errors %} +

{{ error }}

+ {% endfor %} +

PDF, JPG eller PNG – max 10 MB.

+
+
+
+ {% endfor %} +
+ +
+

+ När du skickar in skickas du vidare mot adminvyn. Saknar du inloggning får du möjlighet att logga in. +

+ +
+ +{% with empty_form=formset.empty_form %} + +{% endwith %} diff --git a/claims/templates/claims/submit_claim.html b/claims/templates/claims/submit_claim.html index ce3adac..68bbc73 100644 --- a/claims/templates/claims/submit_claim.html +++ b/claims/templates/claims/submit_claim.html @@ -3,52 +3,124 @@ {% block title %}Skicka utlägg{% endblock %} {% block content %} -

Skicka in utlägg

-

Formuläret är öppet för alla. Du kan fylla i flera rader innan du skickar in.

-

Behöver du fler rader än som visas? Lägg till ?forms=n i URL:en (max {{ max_extra_forms }}).

-
+
+
+

Utlägg

+

Skicka in dina kostnader

+

+ Formuläret fungerar både för inloggade och gäster. Varje rad nedan motsvarar ett utlägg. + Behöver du fler rader? Lägg till ?forms=n i URL:en (max {{ max_extra_forms }}). +

+
+ + {% csrf_token %} -
- Dina uppgifter - {{ claimant_form.as_p }} -
- {{ formset.management_form }} - {% for form in formset %} -
- Utlägg {{ forloop.counter }} - {{ form.non_field_errors }} - {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} -

- {{ form.description.label_tag }} - {{ form.description }} - {{ form.description.errors }} -

-

- {{ form.amount.label_tag }} - {{ form.amount }} - {{ form.amount.errors }} -

-
- Avancerat: ändra valuta (standard SEK) -

- {{ form.currency.label_tag }} - {{ form.currency }} - {{ form.currency.errors }} -

-

- {{ form.project.label_tag }} - {{ form.project }} - {{ form.project.errors }} -

-
-

- {{ form.receipt.label_tag }} - {{ form.receipt }} - {{ form.receipt.errors }} -

-
- {% endfor %} - + +
+
+

Steg 1

+

Dina uppgifter

+

Vi återkommer via dessa kontaktuppgifter och använder kontonumret för utbetalning.

+
+
+ {% for field in claimant_form %} + {% if field.is_hidden %} + {{ field }} + {% else %} +
+ + {{ field }} + {% if field.help_text %} +

{{ field.help_text }}

+ {% endif %} + {% for error in field.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} + {% endfor %} +
+
+ +
+ {% include "claims/includes/claim_formset.html" %} +
-

När du skickar formuläret lotsas du till adminvyn. Saknar du inloggning får du möjlighet att logga in.

+
+ + {% endblock %} diff --git a/claims/views.py b/claims/views.py index d5c80be..3e0716c 100644 --- a/claims/views.py +++ b/claims/views.py @@ -52,20 +52,28 @@ class SubmitClaimView(View): initial["account_number"] = last_claim.account_number return initial + def build_context(self, formset, claimant_form): + current_forms = formset.total_form_count() + return { + "formset": formset, + "claimant_form": claimant_form, + "current_forms": current_forms, + "max_extra_forms": self.max_extra_forms, + "can_add_forms": current_forms < self.max_extra_forms, + "can_remove_forms": current_forms > 1, + "add_forms_value": min(self.max_extra_forms, current_forms + 1), + "remove_forms_value": max(1, current_forms - 1), + "form_fragment": "claim-formset", + } + def get(self, request): extra = self.get_extra_forms() formset = self.build_formset(extra=extra) claimant_form = ClaimantForm(initial=self.get_claimant_initial()) - return render( - request, - self.template_name, - { - "formset": formset, - "claimant_form": claimant_form, - "extra_forms": extra, - "max_extra_forms": self.max_extra_forms, - }, - ) + context = self.build_context(formset, claimant_form) + if self._wants_fragment(request): + return render(request, "claims/includes/claim_formset.html", context) + return render(request, self.template_name, context) def post(self, request): formset = self.build_formset(data=request.POST, files=request.FILES) @@ -108,16 +116,13 @@ class SubmitClaimView(View): else: messages.error(request, "Kunde inte spara utläggen. Kontrollera formuläret.") - extra = min(formset.total_form_count(), self.max_extra_forms) - return render( - request, - self.template_name, - { - "formset": formset, - "claimant_form": claimant_form, - "extra_forms": extra, - "max_extra_forms": self.max_extra_forms, - }, + return render(request, self.template_name, self.build_context(formset, claimant_form)) + + @staticmethod + def _wants_fragment(request): + return ( + request.headers.get("x-requested-with") == "XMLHttpRequest" + or request.GET.get("fragment") == "claim-formset" )