Squash merge feature/email-notifications into beta
This commit is contained in:
@@ -23,7 +23,12 @@ Bygg ett webbaserat system för hantering av utlägg (”claims”) åt en organ
|
||||
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).
|
||||
13. Hantering av utbetalningar i UI är bakom flaggan `CLAIMS_ENABLE_INTERNAL_PAYMENTS` och kan även togglas via Django admin (modell `SystemSetting`). När den är på ska godkända claims få en summeringssektion med tydlig info (namn, belopp, kontonr) och en "Betala"-knapp som markerar posten som betald (med logg och markerad betalstatus). När flaggan är av saknas knappen och admins instrueras att hantera betalning externt.
|
||||
14. När ett utlägg markerats som betalt ska beslut/status vara låst (ingen uppdatering av kommentar eller status i UI eller Django admin).
|
||||
14. När ett utlägg markerats som betalt ska beslut/status vara låst (ingen uppdatering av kommentar eller status i UI eller Django admin). Reset av betalstatus sker endast via admin-knappen.
|
||||
15. Filuppladdningar ska alltid få säkra, unika namn (UUID i `receipts/`), och originalnamn ska inte exponeras.
|
||||
16. För e-postaviseringar: använd `CLAIMS_EMAIL_ENABLED` (default false) och miljövariablerna `EMAIL_HOST`, `EMAIL_PORT`, `EMAIL_USE_TLS`, `EMAIL_HOST_USER`, `EMAIL_HOST_PASSWORD`, `CLAIMS_EMAIL_FROM`, `CLAIMS_ADMIN_NOTIFICATION_EMAIL`. När flaggan är av ska koden inte försöka skicka mejl (men gärna logga att aviseringar är inaktiverade).
|
||||
17. Systemet ska vara tvåspråkigt (sv/en). Alla nya strängar måste wraps i `gettext`/`{% trans %}` och översättningar läggs till via `makemessages`/`compilemessages`. Navbaren innehåller alltid språkväljare och `<html>` ska sätta `lang` enligt vald session.
|
||||
18. Efter inskick ska användaren hamna på en Tailwind-baserad bekräftelsesida med knappar för att logga in eller skicka nytt utlägg.
|
||||
19. Ett management-kommando `reset_claims` ska alltid finnas uppdaterat för att rensa claims men lämna konton (använd `uv run python manage.py reset_claims`).
|
||||
|
||||
## Säkerhet och drift
|
||||
- Skydda admin-flöden bakom inloggning.
|
||||
|
||||
106
README.md
106
README.md
@@ -1,23 +1,91 @@
|
||||
## claims-system
|
||||
|
||||
### Kom igång
|
||||
```bash
|
||||
uv sync
|
||||
uv run python manage.py migrate
|
||||
uv run python manage.py createsuperuser
|
||||
uv run python manage.py runserver
|
||||
Modern Django/Tailwind-baserad portal för att ta emot, granska och betala utlägg.
|
||||
|
||||
---
|
||||
|
||||
### 1. Kom igång
|
||||
1. `uv sync`
|
||||
2. `uv run python manage.py migrate`
|
||||
3. `uv run python manage.py createsuperuser`
|
||||
4. `uv run python manage.py runserver`
|
||||
|
||||
Nyckel-URLer (default):
|
||||
- Offentligt formulär `GET /claims/new/`
|
||||
- Bekräftelsesida `GET /claims/submitted/`
|
||||
- Adminlista `GET /claims/admin/`
|
||||
- Mina utlägg `GET /claims/mine/`
|
||||
- Användarhantering `GET /claims/users/`
|
||||
- Export-placeholder `GET /claims/export/`
|
||||
- Auth `GET /accounts/login|logout/`
|
||||
|
||||
---
|
||||
|
||||
### 2. Kärnfunktioner
|
||||
- **Multi-rad formulär:** Offentligt formulär stödjer upp till 5 rader. Lägg till `?forms=n` eller använd +/−-knapparna (lägger till rader utan reload).
|
||||
- **Auto-prefill:** Inloggade användare får namn, e-post och senaste kontonummer förifyllt.
|
||||
- **Valuta & projekt:** Varje rad har dold valutaväljare (SEK default) och projektreferens. Projekt listas från Django admin > Projekt.
|
||||
- **Kvitton:** Filuppladdningar sparas med slumpat UUID-baserat namn under `receipts/` för säkerhet och unika namn.
|
||||
- **Adminlista:** Kortlayout med statuschippar, loggtimeline, kvittolänkar och inline-formulär för godkänn/avslag.
|
||||
- **Betalspårning:** När intern betalning är på får godkända claims en "Betala"-knapp. När ett claim markeras som betalt låses status/kommentar tills reset görs.
|
||||
- **Mina utlägg:** Inloggade ser sina egna claims i samma Tailwind-layout med kvitto-länk och logg.
|
||||
- **Användarhantering:** Tailwind-sida där personal kan skapa konton, tilldela `claims.view_claim`/`claims.change_claim`, markera staff och ta bort användare.
|
||||
|
||||
---
|
||||
|
||||
### 3. Språk & UI
|
||||
- Django i18n är aktiverat (`LANGUAGES = [('sv','Swedish'), ('en','English')]`, LocaleMiddleware, språkväljare i navbaren).
|
||||
- Alla mallar/formulär använder `{% trans %}`/`gettext`. Engelska översättningar ligger i `locale/en/LC_MESSAGES/django.po` (kompileras till `.mo`).
|
||||
- Uppdatera översättningar:
|
||||
```bash
|
||||
uv run django-admin makemessages -l en
|
||||
uv run django-admin compilemessages -l en
|
||||
```
|
||||
- `<html lang="{{ LANGUAGE_CODE }}">` sätts automatiskt, och språkväljaren lagrar valet i session/cookie.
|
||||
|
||||
---
|
||||
|
||||
### 4. Viktiga inställningar
|
||||
| Variabel | Default | Beskrivning |
|
||||
| --- | --- | --- |
|
||||
| `CLAIMS_ENABLE_INTERNAL_PAYMENTS` | `true` | Styr “Betala”-flödet. Kan också togglas i Django admin > Systeminställningar. |
|
||||
| `CLAIMS_EMAIL_ENABLED` | `false` | Slår på e-postaviseringar. Låt vara `false` i testläge. |
|
||||
| `CLAIMS_EMAIL_FROM` | `no-reply@claims.local` | Avsändare för utskick. |
|
||||
| `CLAIMS_ADMIN_NOTIFICATION_EMAIL` | tom | Om satt skickas notifiering vid nytt claim (när e-post är aktiverad). |
|
||||
| `EMAIL_BACKEND` | `django.core.mail.backends.console.EmailBackend` | Byt till SMTP i prod (se nedan). |
|
||||
| `EMAIL_HOST`, `EMAIL_PORT`, `EMAIL_USE_TLS`, `EMAIL_HOST_USER`, `EMAIL_HOST_PASSWORD` | tom | Standard Django SMTP-inställningar. |
|
||||
| `LANGUAGE_CODE` | `sv` | Standardspråk. |
|
||||
| `LOCALE_PATHS` | `BASE_DIR/locale` | Katalog för `.po/.mo`. |
|
||||
| `LOGIN_REDIRECT_URL` | `/claims/admin/` | Efter inloggning. |
|
||||
| `LOGOUT_REDIRECT_URL` | `/accounts/login/` | Efter utloggning. |
|
||||
|
||||
SMTP-exempel:
|
||||
```env
|
||||
CLAIMS_EMAIL_ENABLED=true
|
||||
CLAIMS_EMAIL_FROM=claims@example.com
|
||||
CLAIMS_ADMIN_NOTIFICATION_EMAIL=finance@example.com
|
||||
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
|
||||
EMAIL_HOST=smtp.example.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_USE_TLS=true
|
||||
EMAIL_HOST_USER=apikey
|
||||
EMAIL_HOST_PASSWORD=secret
|
||||
```
|
||||
|
||||
- Offentligt formulär: `http://localhost:8000/claims/new/`
|
||||
- Sidan börjar med ett block där användaren skriver sina uppgifter (för inloggade fylls namn/e‑post + senaste kontonummer i automatiskt). Själva utläggsraderna kan fyllas i flera åt gången via formset (lägg till `?forms=n` för fler rader, max 5).
|
||||
- Varje rad har en dold valuta-väljare. Standard är SEK men EUR/USD/GBP går att välja vid behov.
|
||||
- Välj även vilket projekt/evenemang utlägget hör till (valen hämtas från Django admin > Projekt).
|
||||
- Adminlista (kräver `claims.view_claim`, uppdateringar kräver `claims.change_claim`): `http://localhost:8000/claims/admin/`
|
||||
- Adminlistan visar kvittolänk, vem som skickade in (och om det var en inloggad användare) samt en logg över alla statusändringar.
|
||||
- När ett utlägg markerats som betalat låses beslutskommentar/status i hela systemet (både listvyn och Django admin).
|
||||
- Export-meny (placeholder för framtida integrationer): `http://localhost:8000/claims/export/`
|
||||
- Inloggade användare kan följa sina egna claim via `http://localhost:8000/claims/mine/`.
|
||||
- Behörighets- och kontohantering (visa kräver `auth.view_user`, skapa/uppdatera/ta bort kräver respektive `auth.add_user`/`auth.change_user`/`auth.delete_user`): `http://localhost:8000/claims/users/`
|
||||
- Django auth-vyer (login/logout) exponeras under `/accounts/`.
|
||||
- Använd Django admin (`/admin/`) för att skapa konton, lägga användare i grupper, lägga upp projekt/evenemang samt tilldela behörigheterna `claims.view_claim` och `claims.change_claim`. Superusers har full kontroll per default.
|
||||
- Intern betalningshantering styrs av miljövariabeln `CLAIMS_ENABLE_INTERNAL_PAYMENTS` (default `true`) och kan dessutom togglas i Django admin under **Systeminställningar**. När funktionen är på får godkända claims en "Betala"-knapp som loggar vem som markerade posten som betald.
|
||||
---
|
||||
|
||||
### 5. Underhåll & verktyg
|
||||
- **Reset claims:** `uv run python manage.py reset_claims` (bekräfta med `ja` eller använd `--noinput`). Tar bort alla claims/loggar/kvitton men lämnar användarkonton.
|
||||
- **Django admin:** `/admin/` används för projekt, grupper, superusers, SystemSetting mm. Staff-användare har automatiskt åtkomst.
|
||||
- **Testa konfiguration:** `uv run python manage.py check`.
|
||||
- **Språkreset:** Rensa cookies om språkväljaren inte byter språk (Django använder `django_language` cookien).
|
||||
|
||||
---
|
||||
|
||||
### 6. Länkar och roller
|
||||
- Offentligt formulär: alla (även utan konto).
|
||||
- Mina utlägg / Adminlista / Export / Betalningar: kräver `claims.view_claim` (och `claims.change_claim` för beslut).
|
||||
- Användarhantering: `auth.view_user` + respektive add/change/delete.
|
||||
- Språkväljare och logout finns i nav-menyn på varje sida.
|
||||
|
||||
Se även `AGENTS.md` för utvecklingsriktlinjer, betalflöden och e-postpolicy. EOF
|
||||
|
||||
53
claims/email_utils.py
Normal file
53
claims/email_utils.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.mail import send_mail
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def emails_enabled():
|
||||
return getattr(settings, "CLAIMS_EMAIL_ENABLED", False)
|
||||
|
||||
|
||||
def _send(subject: str, template: str, context: dict, recipient: str):
|
||||
if not emails_enabled():
|
||||
logger.info("Email disabled - skipping send to %s", recipient)
|
||||
return
|
||||
html_message = render_to_string(template, context)
|
||||
plain_message = strip_tags(html_message)
|
||||
send_mail(
|
||||
subject=subject,
|
||||
message=plain_message,
|
||||
from_email=settings.CLAIMS_EMAIL_FROM,
|
||||
recipient_list=[recipient],
|
||||
html_message=html_message,
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
|
||||
def send_claimant_confirmation_email(claim):
|
||||
if not claim.email:
|
||||
return
|
||||
context = {"claim": claim}
|
||||
_send(
|
||||
subject="Din utläggsbegäran är mottagen",
|
||||
template="emails/claim_submitted_claimant.html",
|
||||
context=context,
|
||||
recipient=claim.email,
|
||||
)
|
||||
|
||||
|
||||
def notify_admin_of_claim(claim):
|
||||
recipient = settings.CLAIMS_ADMIN_NOTIFICATION_EMAIL
|
||||
if not recipient:
|
||||
return
|
||||
context = {"claim": claim}
|
||||
_send(
|
||||
subject="Nytt utlägg inskickat",
|
||||
template="emails/claim_submitted_admin.html",
|
||||
context=context,
|
||||
recipient=recipient,
|
||||
)
|
||||
@@ -1,6 +1,7 @@
|
||||
from django import forms
|
||||
from django.contrib.auth import get_user_model, password_validation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Claim, Project
|
||||
|
||||
@@ -15,16 +16,16 @@ FILE_CLASSES = "mt-1 block w-full text-sm text-gray-700 file:mr-4 file:rounded-m
|
||||
class ClaimantForm(forms.Form):
|
||||
full_name = forms.CharField(
|
||||
max_length=255,
|
||||
label="Namn",
|
||||
label=_("Namn"),
|
||||
widget=forms.TextInput(attrs={"class": INPUT_CLASSES}),
|
||||
)
|
||||
email = forms.EmailField(
|
||||
label="E-post",
|
||||
label=_("E-post"),
|
||||
widget=forms.EmailInput(attrs={"class": INPUT_CLASSES}),
|
||||
)
|
||||
account_number = forms.CharField(
|
||||
max_length=50,
|
||||
label="Kontonummer",
|
||||
label=_("Kontonummer"),
|
||||
widget=forms.TextInput(attrs={"class": INPUT_CLASSES}),
|
||||
)
|
||||
|
||||
@@ -45,11 +46,11 @@ class ClaimLineForm(forms.ModelForm):
|
||||
model = Claim
|
||||
fields = ["description", "amount", "currency", "project", "receipt"]
|
||||
labels = {
|
||||
"description": "Beskrivning",
|
||||
"amount": "Belopp",
|
||||
"currency": "Valuta",
|
||||
"project": "Evenemang/Projekt",
|
||||
"receipt": "Kvitto",
|
||||
"description": _("Beskrivning"),
|
||||
"amount": _("Belopp"),
|
||||
"currency": _("Valuta"),
|
||||
"project": _("Evenemang/Projekt"),
|
||||
"receipt": _("Kvitto"),
|
||||
}
|
||||
widgets = {
|
||||
"description": forms.Textarea(attrs={"rows": 3, "class": TEXTAREA_CLASSES}),
|
||||
@@ -60,15 +61,15 @@ class ClaimDecisionForm(forms.Form):
|
||||
ACTION_APPROVE = "approve"
|
||||
ACTION_REJECT = "reject"
|
||||
ACTION_CHOICES = (
|
||||
(ACTION_APPROVE, "Godkänn"),
|
||||
(ACTION_REJECT, "Neka"),
|
||||
(ACTION_APPROVE, _("Godkänn")),
|
||||
(ACTION_REJECT, _("Neka")),
|
||||
)
|
||||
|
||||
claim_id = forms.IntegerField(widget=forms.HiddenInput)
|
||||
action = forms.ChoiceField(choices=ACTION_CHOICES)
|
||||
decision_note = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.Textarea(attrs={"rows": 2, "placeholder": "Kommentar"}),
|
||||
widget=forms.Textarea(attrs={"rows": 2, "placeholder": _("Kommentar")}),
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
@@ -76,31 +77,31 @@ class ClaimDecisionForm(forms.Form):
|
||||
action = cleaned.get("action")
|
||||
note = cleaned.get("decision_note", "").strip()
|
||||
if action == self.ACTION_REJECT and not note:
|
||||
self.add_error("decision_note", "Kommentar krävs när du nekar ett utlägg.")
|
||||
self.add_error("decision_note", _("Kommentar krävs när du nekar ett utlägg."))
|
||||
return cleaned
|
||||
|
||||
|
||||
class UserManagementForm(forms.Form):
|
||||
username = forms.CharField(max_length=150, label="Användarnamn")
|
||||
email = forms.EmailField(required=False, label="E-post")
|
||||
first_name = forms.CharField(max_length=150, required=False, label="Förnamn")
|
||||
last_name = forms.CharField(max_length=150, required=False, label="Efternamn")
|
||||
password1 = forms.CharField(widget=forms.PasswordInput, label="Lösenord")
|
||||
password2 = forms.CharField(widget=forms.PasswordInput, label="Bekräfta lösenord")
|
||||
is_staff = forms.BooleanField(required=False, initial=True, label="Administratör (staff)")
|
||||
grant_view = forms.BooleanField(required=False, initial=True, label="Ge behörighet att se utlägg")
|
||||
grant_change = forms.BooleanField(required=False, initial=True, label="Ge behörighet att besluta utlägg")
|
||||
username = forms.CharField(max_length=150, label=_("Användarnamn"))
|
||||
email = forms.EmailField(required=False, label=_("E-post"))
|
||||
first_name = forms.CharField(max_length=150, required=False, label=_("Förnamn"))
|
||||
last_name = forms.CharField(max_length=150, required=False, label=_("Efternamn"))
|
||||
password1 = forms.CharField(widget=forms.PasswordInput, label=_("Lösenord"))
|
||||
password2 = forms.CharField(widget=forms.PasswordInput, label=_("Bekräfta lösenord"))
|
||||
is_staff = forms.BooleanField(required=False, initial=True, label=_("Administratör (staff)"))
|
||||
grant_view = forms.BooleanField(required=False, initial=True, label=_("Ge behörighet att se utlägg"))
|
||||
grant_change = forms.BooleanField(required=False, initial=True, label=_("Ge behörighet att besluta utlägg"))
|
||||
|
||||
def clean_username(self):
|
||||
username = self.cleaned_data["username"]
|
||||
if User.objects.filter(username=username).exists():
|
||||
raise forms.ValidationError("Användarnamnet är upptaget.")
|
||||
raise forms.ValidationError(_("Användarnamnet är upptaget."))
|
||||
return username
|
||||
|
||||
def clean(self):
|
||||
cleaned = super().clean()
|
||||
if cleaned.get("password1") != cleaned.get("password2"):
|
||||
self.add_error("password2", "Lösenorden matchar inte.")
|
||||
self.add_error("password2", _("Lösenorden matchar inte."))
|
||||
password = cleaned.get("password1")
|
||||
if password:
|
||||
temp_user = User(
|
||||
@@ -118,9 +119,9 @@ class UserManagementForm(forms.Form):
|
||||
|
||||
class UserPermissionForm(forms.Form):
|
||||
user_id = forms.IntegerField(widget=forms.HiddenInput)
|
||||
is_staff = forms.BooleanField(required=False, label="Admin/staff")
|
||||
grant_view = forms.BooleanField(required=False, label="Får se utlägg")
|
||||
grant_change = forms.BooleanField(required=False, label="Får besluta utlägg")
|
||||
is_staff = forms.BooleanField(required=False, label=_("Admin/staff"))
|
||||
grant_view = forms.BooleanField(required=False, label=_("Får se utlägg"))
|
||||
grant_change = forms.BooleanField(required=False, label=_("Får besluta utlägg"))
|
||||
|
||||
|
||||
class DeleteUserForm(forms.Form):
|
||||
|
||||
1
claims/management/__init__.py
Normal file
1
claims/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Package marker
|
||||
1
claims/management/commands/__init__.py
Normal file
1
claims/management/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Package marker
|
||||
35
claims/management/commands/reset_claims.py
Normal file
35
claims/management/commands/reset_claims.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
|
||||
from claims.models import Claim
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Tar bort samtliga utlägg (claims) men lämnar användarkonton orörda."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--noinput",
|
||||
action="store_true",
|
||||
help="Utför rensningen utan interaktiv bekräftelse.",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if not options["noinput"]:
|
||||
confirm = input(
|
||||
"Detta tar bort alla claims inklusive loggar och kvitton men lämnar konton. Fortsätt? (skriv 'ja'): "
|
||||
)
|
||||
if confirm.lower() != "ja":
|
||||
self.stdout.write(self.style.WARNING("Avbrutet."))
|
||||
return
|
||||
|
||||
count = Claim.objects.count()
|
||||
with transaction.atomic():
|
||||
for claim in Claim.objects.iterator():
|
||||
if claim.receipt:
|
||||
claim.receipt.delete(save=False)
|
||||
Claim.objects.all().delete()
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"Raderade {count} claims och tillhörande loggar/kvitton.")
|
||||
)
|
||||
@@ -1,4 +1,7 @@
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -90,6 +93,22 @@ class Claim(models.Model):
|
||||
performed_by=performed_by,
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.receipt and not kwargs.pop("skip_receipt_rename", False):
|
||||
original_name = self.receipt.name or ""
|
||||
ext = original_name.rsplit(".", 1)[-1] if "." in original_name else "dat"
|
||||
new_name = self._generate_unique_receipt_name(ext)
|
||||
self.receipt.name = new_name
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def _generate_unique_receipt_name(self, ext):
|
||||
ext = ext.lower()
|
||||
for _ in range(10):
|
||||
candidate = f"receipts/{uuid.uuid4().hex}.{ext}"
|
||||
if not default_storage.exists(candidate):
|
||||
return candidate
|
||||
return f"receipts/{uuid.uuid4().hex}.{ext}"
|
||||
|
||||
|
||||
class ClaimLog(models.Model):
|
||||
class Action(models.TextChoices):
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
{% extends "claims/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Admin – Utlägg{% endblock %}
|
||||
{% block title %}{% trans "Admin – Utlägg" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="space-y-8 py-6">
|
||||
<header class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">Översikt</p>
|
||||
<h1 class="text-3xl font-semibold text-gray-900">Inkomna utlägg</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">Filtrera på status, granska kvitton och uppdatera beslut direkt i listan.</p>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">{% trans "Översikt" %}</p>
|
||||
<h1 class="text-3xl font-semibold text-gray-900">{% trans "Inkomna utlägg" %}</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">{% trans "Filtrera på status, granska kvitton och uppdatera beslut direkt i listan." %}</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% with selected=status_filter %}
|
||||
{% with filters="all"|add:"," %}
|
||||
{% endwith %}
|
||||
{% with statuses=status_choices %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
<a href="?status=all"
|
||||
class="rounded-full px-4 py-2 text-sm font-semibold {% if status_filter == 'all' %}bg-brand-600 text-white{% else %}bg-white text-gray-700 shadow-sm ring-1 ring-gray-200 hover:bg-gray-50{% endif %}">
|
||||
Alla
|
||||
{% trans "Alla" %}
|
||||
</a>
|
||||
{% for value, label in status_choices %}
|
||||
<a href="?status={{ value }}"
|
||||
@@ -45,17 +40,17 @@
|
||||
{{ claim.project }}
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="text-xs text-gray-400">Skapad {{ claim.created_at|date:"Y-m-d H:i" }}</span>
|
||||
<span class="text-xs text-gray-400">{% trans "Skapad" %} {{ claim.created_at|date:"Y-m-d H:i" }}</span>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">Person</p>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">{% trans "Person" %}</p>
|
||||
<h2 class="text-2xl font-semibold text-gray-900">{{ claim.full_name }}</h2>
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ claim.email }} · Konto: <span class="font-mono text-gray-900">{{ claim.account_number }}</span><br>
|
||||
{{ claim.email }} · {% trans "Konto" %}: <span class="font-mono text-gray-900">{{ claim.account_number }}</span><br>
|
||||
{% if claim.submitted_by %}
|
||||
<span class="text-xs uppercase tracking-wide text-green-600">Inloggad användare: {{ claim.submitted_by.get_username }}</span>
|
||||
<span class="text-xs uppercase tracking-wide text-green-600">{% trans "Inloggad användare" %}: {{ claim.submitted_by.get_username }}</span>
|
||||
{% else %}
|
||||
<span class="text-xs uppercase tracking-wide text-gray-500">Inskickad av gäst</span>
|
||||
<span class="text-xs uppercase tracking-wide text-gray-500">{% trans "Inskickad av gäst" %}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
@@ -65,16 +60,16 @@
|
||||
{{ claim.get_status_display }}
|
||||
</span>
|
||||
{% if claim.decision_note %}
|
||||
<p class="text-xs text-gray-500">Kommentar: {{ claim.decision_note }}</p>
|
||||
<p class="text-xs text-gray-500">{% trans "Kommentar" %}: {{ claim.decision_note }}</p>
|
||||
{% endif %}
|
||||
{% if payments_enabled and claim.status == 'approved' %}
|
||||
{% if claim.is_paid %}
|
||||
<span class="rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold text-emerald-800">
|
||||
Betald {{ claim.paid_at|date:"Y-m-d H:i" }}
|
||||
{% if claim.paid_by %}av {{ claim.paid_by.get_username }}{% endif %}
|
||||
{% trans "Betald" %} {{ claim.paid_at|date:"Y-m-d H:i" }}
|
||||
{% if claim.paid_by %}{% trans "av" %} {{ claim.paid_by.get_username }}{% endif %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-xs text-gray-500">Ej markerad som betald</span>
|
||||
<span class="text-xs text-gray-500">{% trans "Ej markerad som betald" %}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -84,25 +79,25 @@
|
||||
<div class="mx-6 mt-4 rounded-2xl border border-green-100 bg-green-50 px-6 py-4 text-sm text-green-900">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-green-600">Sammanfattning</p>
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-green-600">{% trans "Sammanfattning" %}</p>
|
||||
<p class="text-lg font-semibold text-green-900">{{ claim.full_name }}</p>
|
||||
<p class="text-sm text-green-800">Belopp: <strong>{{ claim.amount }} {{ claim.currency }}</strong> · Konto: <span class="font-mono">{{ claim.account_number }}</span></p>
|
||||
<p class="text-sm text-green-800">{% trans "Belopp" %}: <strong>{{ claim.amount }} {{ claim.currency }}</strong> · {% trans "Konto" %}: <span class="font-mono">{{ claim.account_number }}</span></p>
|
||||
</div>
|
||||
{% if payments_enabled %}
|
||||
{% if claim.is_paid %}
|
||||
<p class="text-xs uppercase tracking-wide text-emerald-600">Markerad som betald</p>
|
||||
<p class="text-xs uppercase tracking-wide text-emerald-600">{% trans "Markerad som betald" %}</p>
|
||||
{% else %}
|
||||
<form method="post" onsubmit="return confirm('Är du säker på att du har lagt upp betalningen? Markera endast som betald om beloppet skickas till banken.');">
|
||||
<form method="post" onsubmit="return confirm('{% trans "Är du säker på att du har lagt upp betalningen? Markera endast som betald om beloppet skickas till banken." %}');">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action_type" value="payment">
|
||||
<input type="hidden" name="payment_claim_id" value="{{ claim.id }}">
|
||||
<button type="submit" class="inline-flex items-center gap-2 rounded-full bg-emerald-600 px-4 py-2 text-xs font-semibold uppercase tracking-wide text-white transition hover:bg-emerald-700">
|
||||
Betala
|
||||
{% trans "Betala" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="text-xs text-green-700">Intern betalningshantering är av – markera betalning i ekonomisystemet.</p>
|
||||
<p class="text-xs text-green-700">{% trans "Intern betalningshantering är av – markera betalning i ekonomisystemet." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,7 +105,7 @@
|
||||
|
||||
<div class="grid gap-6 px-6 py-6 lg:grid-cols-3">
|
||||
<div class="lg:col-span-2">
|
||||
<p class="text-sm font-semibold text-gray-500">Beskrivning</p>
|
||||
<p class="text-sm font-semibold text-gray-500">{% trans "Beskrivning" %}</p>
|
||||
<p class="mt-2 whitespace-pre-wrap text-gray-800">{{ claim.description }}</p>
|
||||
<div class="mt-4 flex flex-wrap items-center gap-4 text-sm text-gray-600">
|
||||
{% if claim.receipt %}
|
||||
@@ -118,17 +113,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Visa kvitto
|
||||
{% trans "Visa kvitto" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-xs text-gray-400">Inget kvitto bifogat</span>
|
||||
<span class="text-xs text-gray-400">{% trans "Inget kvitto bifogat" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4 rounded-2xl bg-slate-50 p-5">
|
||||
<details class="group">
|
||||
<summary class="cursor-pointer select-none text-sm font-semibold text-gray-700">
|
||||
Logg & tidslinje
|
||||
{% trans "Logg & tidslinje" %}
|
||||
</summary>
|
||||
<ul class="mt-3 space-y-2 text-sm text-gray-600">
|
||||
{% for log in claim.logs.all %}
|
||||
@@ -136,17 +131,17 @@
|
||||
<p class="font-semibold text-gray-900">{{ log.get_action_display }}</p>
|
||||
<p class="text-xs text-gray-500">{{ log.created_at|date:"Y-m-d H:i" }}</p>
|
||||
{% if log.from_status %}
|
||||
<p class="text-xs text-gray-500">Status: {{ log.get_from_status_display }} → {{ log.get_to_status_display }}</p>
|
||||
<p class="text-xs text-gray-500">{% trans "Status" %}: {{ log.get_from_status_display }} → {{ log.get_to_status_display }}</p>
|
||||
{% endif %}
|
||||
{% if log.note %}
|
||||
<p class="mt-1 text-xs text-gray-600">"{{ log.note }}"</p>
|
||||
{% endif %}
|
||||
{% if log.performed_by %}
|
||||
<p class="text-xs text-gray-400">Av {{ log.performed_by.get_username }}</p>
|
||||
<p class="text-xs text-gray-400">{% trans "Av" %} {{ log.performed_by.get_username }}</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% empty %}
|
||||
<li class="text-xs text-gray-400">Ingen logg än.</li>
|
||||
<li class="text-xs text-gray-400">{% trans "Ingen logg än." %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
@@ -154,26 +149,26 @@
|
||||
{% if can_change %}
|
||||
{% if claim.is_paid %}
|
||||
<p class="rounded-lg bg-slate-100 px-3 py-2 text-xs text-slate-600">
|
||||
Utlägget är markerat som betalt. Ändringar av beslut/kommentar är låsta.
|
||||
{% trans "Utlägget är markerat som betalt. Ändringar av beslut/kommentar är låsta." %}
|
||||
</p>
|
||||
{% else %}
|
||||
<form method="post" class="space-y-3">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="claim_id" value="{{ claim.id }}">
|
||||
|
||||
<label class="block text-sm font-medium text-gray-700">Åtgärd</label>
|
||||
<select name="action" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-brand-600">
|
||||
<label class="block text-sm font-medium text-gray-700">{% trans "Åtgärd" %}</label>
|
||||
<select name="action" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-brand-600 focus:outline-none focus:ring-2 focus:ring-brand-600">
|
||||
{% for value, label in decision_choices %}
|
||||
<option value="{{ value }}">{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label class="block text-sm font-medium text-gray-700">Kommentar</label>
|
||||
<textarea name="decision_note" rows="3" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-brand-600">{{ claim.decision_note }}</textarea>
|
||||
<label class="block text-sm font-medium text-gray-700">{% trans "Kommentar" %}</label>
|
||||
<textarea name="decision_note" rows="3" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-brand-600 focus:outline-none focus:ring-2 focus:ring-brand-600">{{ claim.decision_note }}</textarea>
|
||||
|
||||
<input type="hidden" name="action_type" value="decision">
|
||||
<button type="submit" class="w-full rounded-full bg-brand-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-brand-700">
|
||||
Uppdatera beslut
|
||||
{% trans "Uppdatera beslut" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
@@ -185,8 +180,8 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-gray-200 bg-white px-6 py-10 text-center text-gray-500">
|
||||
<p class="text-lg font-semibold text-gray-900">Inga utlägg ännu</p>
|
||||
<p class="mt-2 text-sm">När formuläret tas emot visas posterna automatiskt här.</p>
|
||||
<p class="text-lg font-semibold text-gray-900">{% trans "Inga utlägg ännu" %}</p>
|
||||
<p class="mt-2 text-sm">{% trans "När formuläret tas emot visas posterna automatiskt här." %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{% load i18n %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="sv">
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<html lang="{{ LANGUAGE_CODE }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Claims{% endblock %}</title>
|
||||
<title>{% block title %}{% trans "Claims" %}{% endblock %}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
@@ -24,37 +26,55 @@
|
||||
<body class="min-h-screen bg-slate-50 text-gray-900">
|
||||
<header class="bg-white shadow-sm">
|
||||
<div class="mx-auto flex max-w-6xl items-center justify-between px-4 py-4">
|
||||
<a href="{% url 'claims:submit' %}" class="text-lg font-semibold text-gray-900 hover:text-brand-700">claims-system</a>
|
||||
<nav class="flex flex-wrap items-center gap-3 text-sm font-medium text-gray-600">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="rounded-full border border-gray-200 bg-gray-50 px-3 py-1 text-xs uppercase tracking-wide text-gray-500">Offentlig</span>
|
||||
<a class="hover:text-gray-900" href="{% url 'claims:submit' %}">Skicka utlägg</a>
|
||||
</div>
|
||||
<a href="{% url 'claims:submit' %}" class="text-lg font-semibold text-gray-900 hover:text-brand-700">{% trans "claims-system" %}</a>
|
||||
<nav class="flex flex-wrap items-center gap-4 text-sm font-medium text-gray-600">
|
||||
<a class="rounded-full border border-gray-200 px-3 py-1 hover:text-gray-900" href="{% url 'claims:submit' %}">
|
||||
{% trans "Skicka utlägg" %}
|
||||
</a>
|
||||
{% if user.is_authenticated %}
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<span class="rounded-full border border-gray-200 bg-gray-50 px-3 py-1 text-xs uppercase tracking-wide text-gray-500">Intern</span>
|
||||
<a class="hover:text-gray-900" href="{% url 'claims:admin-list' %}">Utläggslista</a>
|
||||
<a class="hover:text-gray-900" href="{% url 'claims:my-claims' %}">Mina utlägg</a>
|
||||
<details class="group relative">
|
||||
<summary class="flex cursor-pointer items-center gap-2 rounded-full border border-gray-200 px-3 py-1 text-sm text-gray-600 transition hover:text-gray-900">
|
||||
{% trans "Interna vyer" %}
|
||||
<svg class="h-3 w-3 transition group-open:rotate-180" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 1.5L5 4.5L9 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</summary>
|
||||
<div class="absolute right-0 z-20 mt-2 w-52 rounded-2xl border border-gray-200 bg-white p-3 shadow-lg">
|
||||
<a class="block rounded-xl px-3 py-2 text-sm text-gray-700 hover:bg-gray-50" href="{% url 'claims:admin-list' %}">{% trans "Utläggslista" %}</a>
|
||||
<a class="block rounded-xl px-3 py-2 text-sm text-gray-700 hover:bg-gray-50" href="{% url 'claims:my-claims' %}">{% trans "Mina utlägg" %}</a>
|
||||
{% if perms.auth.view_user %}
|
||||
<a class="hover:text-gray-900" href="{% url 'claims:user-manage' %}">Användare</a>
|
||||
<a class="block rounded-xl px-3 py-2 text-sm text-gray-700 hover:bg-gray-50" href="{% url 'claims:user-manage' %}">{% trans "Användare" %}</a>
|
||||
{% endif %}
|
||||
<a class="hover:text-gray-900" href="{% url 'claims:export' %}">Export</a>
|
||||
<a class="block rounded-xl px-3 py-2 text-sm text-gray-700 hover:bg-gray-50" href="{% url 'claims:export' %}">{% trans "Export" %}</a>
|
||||
{% if user.is_staff %}
|
||||
<a class="hover:text-gray-900" href="{% url 'admin:index' %}">Django admin</a>
|
||||
<a class="block rounded-xl px-3 py-2 text-sm text-gray-700 hover:bg-gray-50" href="{% url 'admin:index' %}">{% trans "Django admin" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
<form action="{% url 'set_language' %}" method="post" class="inline-flex items-center gap-1 text-xs text-gray-500">
|
||||
{% csrf_token %}
|
||||
<input name="next" type="hidden" value="{{ request.path }}">
|
||||
<label for="lang-select" class="sr-only">{% trans "Språk" %}</label>
|
||||
<select id="lang-select" name="language" onchange="this.form.submit()" class="rounded-full border border-gray-200 bg-white px-3 py-1 text-xs text-gray-700 focus:outline-none">
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
{% get_available_languages as LANGUAGES %}
|
||||
{% for code, name in LANGUAGES %}
|
||||
<option value="{{ code }}"{% if code == LANGUAGE_CODE %} selected{% endif %}>{{ name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
{% if user.is_authenticated %}
|
||||
<span class="hidden text-xs text-gray-400 sm:inline">|</span>
|
||||
<span class="text-xs text-gray-500">Inloggad som {{ user.get_username }}</span>
|
||||
<span class="text-xs text-gray-500">{% trans "Inloggad som" %} {{ user.get_username }}</span>
|
||||
<form action="{% url 'logout' %}" method="post" class="inline">
|
||||
{% csrf_token %}
|
||||
<button class="rounded-full bg-gray-100 px-3 py-1 text-xs font-semibold text-gray-700 transition hover:bg-gray-200" type="submit">
|
||||
Logga ut
|
||||
{% trans "Logga ut" %}
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a class="rounded-full bg-brand-600 px-3 py-1 text-white transition hover:bg-brand-700" href="{% url 'login' %}">Logga in</a>
|
||||
<a class="rounded-full bg-brand-600 px-3 py-1 text-white transition hover:bg-brand-700" href="{% url 'login' %}">{% trans "Logga in" %}</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
{% load i18n %}
|
||||
{% extends "claims/base.html" %}
|
||||
|
||||
{% block title %}Export{% endblock %}
|
||||
{% block title %}{% trans "Export" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Export till redovisningssystem</h1>
|
||||
<p>Detta är ett framtida steg. Här kommer du att kunna:</p>
|
||||
<h1>{% trans "Export till redovisningssystem" %}</h1>
|
||||
<p>{% trans "Detta är ett framtida steg. Här kommer du att kunna:" %}</p>
|
||||
<ul>
|
||||
<li>Välja tidsperiod eller status</li>
|
||||
<li>Exportera till t.ex. bankfil eller SIE</li>
|
||||
<li>Skicka data via API till externa system</li>
|
||||
<li>{% trans "Välja tidsperiod eller status" %}</li>
|
||||
<li>{% trans "Exportera till t.ex. bankfil eller SIE" %}</li>
|
||||
<li>{% trans "Skicka data via API till externa system" %}</li>
|
||||
</ul>
|
||||
<p>Planerade åtgärder:</p>
|
||||
<p>{% trans "Planerade åtgärder:" %}</p>
|
||||
<ol>
|
||||
<li>Definiera format</li>
|
||||
<li>Implementera exportkommando/API</li>
|
||||
<li>Bygga integrationsinställningar</li>
|
||||
<li>{% trans "Definiera format" %}</li>
|
||||
<li>{% trans "Implementera exportkommando/API" %}</li>
|
||||
<li>{% trans "Bygga integrationsinställningar" %}</li>
|
||||
</ol>
|
||||
<p>Tills vidare kan du ladda ner data via Django admin eller med ett enkelt SQL-utdrag.</p>
|
||||
<p>{% trans "Tills vidare kan du ladda ner data via Django admin eller med ett enkelt SQL-utdrag." %}</p>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
{% load i18n %}
|
||||
{{ formset.management_form }}
|
||||
|
||||
<div class="space-y-8" data-formset-list>
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">Steg 2</p>
|
||||
<h2 class="text-2xl font-semibold text-gray-900">Utläggsrader</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">Lägg till ett block per kvitto eller kostnad. Projektväljaren hjälper ekonomin att bokföra rätt.</p>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">{% trans "Steg 2" %}</p>
|
||||
<h2 class="text-2xl font-semibold text-gray-900">{% trans "Utläggsrader" %}</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">{% trans "Lägg till ett block per kvitto eller kostnad. Projektväljaren hjälper ekonomin att bokföra rätt." %}</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<span class="rounded-full bg-gray-200 px-4 py-1 text-sm text-gray-700">
|
||||
Totalt <span data-current-count>{{ current_forms }}</span> rader
|
||||
{% blocktrans %}Totalt <span data-current-count>{{ current_forms }}</span> rader{% endblocktrans %}
|
||||
</span>
|
||||
<div class="flex overflow-hidden rounded-full border border-gray-200 bg-white shadow-sm">
|
||||
<button type="button"
|
||||
@@ -18,7 +19,7 @@
|
||||
{% if not can_remove_forms %}disabled{% endif %}>
|
||||
–
|
||||
</button>
|
||||
<div class="border-l border-r border-gray-200 px-3 py-2 text-sm text-gray-500">justera</div>
|
||||
<div class="border-l border-r border-gray-200 px-3 py-2 text-sm text-gray-500">{% trans "justera" %}</div>
|
||||
<button type="button"
|
||||
class="px-3 py-2 text-sm font-semibold text-gray-600 transition hover:bg-gray-50 {% if not can_add_forms %}pointer-events-none opacity-40{% endif %}"
|
||||
data-action="add-form"
|
||||
@@ -32,8 +33,8 @@
|
||||
{% for form in formset %}
|
||||
<div class="rounded-2xl bg-white shadow-sm ring-1 ring-gray-100" data-claim-card>
|
||||
<div class="border-b border-gray-100 px-6 py-4 flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Utlägg {{ forloop.counter }}</h3>
|
||||
<p class="text-xs text-gray-500">Obligatoriska fält markeras med *</p>
|
||||
<h3 class="text-lg font-semibold text-gray-900">{% blocktrans %}Utlägg {{ forloop.counter }}{% endblocktrans %}</h3>
|
||||
<p class="text-xs text-gray-500">{% trans "Obligatoriska fält markeras med *" %}</p>
|
||||
</div>
|
||||
<div class="space-y-6 px-6 py-6">
|
||||
{{ form.non_field_errors }}
|
||||
@@ -72,7 +73,7 @@
|
||||
|
||||
<details class="rounded-xl border border-dashed border-gray-200 px-4 py-3 text-sm text-gray-600">
|
||||
<summary class="cursor-pointer select-none text-base font-medium text-gray-800">
|
||||
Avancerat: justera valuta (standard SEK)
|
||||
{% trans "Avancerat: justera valuta (standard SEK)" %}
|
||||
</summary>
|
||||
<div class="mt-4 space-y-4">
|
||||
<div>
|
||||
@@ -82,7 +83,7 @@
|
||||
<p class="mt-1 text-sm text-rose-600">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">Använd detta om kvittot är i annan valuta än svenska kronor.</p>
|
||||
<p class="text-xs text-gray-500">{% trans "Använd detta om kvittot är i annan valuta än svenska kronor." %}</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -101,10 +102,10 @@
|
||||
|
||||
<div class="flex items-center justify-between rounded-2xl bg-white p-6 shadow-sm ring-1 ring-gray-100">
|
||||
<p class="text-sm text-gray-600">
|
||||
När du skickar in skickas du vidare mot adminvyn. Saknar du inloggning får du möjlighet att logga in.
|
||||
{% trans "När du skickar in skickas du vidare mot adminvyn. Saknar du inloggning får du möjlighet att logga in." %}
|
||||
</p>
|
||||
<button type="submit" class="inline-flex items-center gap-2 rounded-full bg-brand-600 px-6 py-3 text-sm font-semibold text-white transition hover:bg-brand-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-600 focus-visible:ring-offset-2">
|
||||
Skicka in utlägg
|
||||
{% trans "Skicka in utlägg" %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17.25 8.25L21 12m0 0-3.75 3.75M21 12H3" />
|
||||
</svg>
|
||||
@@ -115,8 +116,8 @@
|
||||
<template id="claim-line-template">
|
||||
<div class="rounded-2xl bg-white shadow-sm ring-1 ring-gray-100" data-claim-card>
|
||||
<div class="border-b border-gray-100 px-6 py-4 flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Ny utläggsrad</h3>
|
||||
<p class="text-xs text-gray-500">Obligatoriska fält markeras med *</p>
|
||||
<h3 class="text-lg font-semibold text-gray-900">{% trans "Ny utläggsrad" %}</h3>
|
||||
<p class="text-xs text-gray-500">{% trans "Obligatoriska fält markeras med *" %}</p>
|
||||
</div>
|
||||
<div class="space-y-6 px-6 py-6">
|
||||
{{ empty_form.non_field_errors }}
|
||||
@@ -146,21 +147,21 @@
|
||||
|
||||
<details class="rounded-xl border border-dashed border-gray-200 px-4 py-3 text-sm text-gray-600">
|
||||
<summary class="cursor-pointer select-none text-base font-medium text-gray-800">
|
||||
Avancerat: justera valuta (standard SEK)
|
||||
{% trans "Avancerat: justera valuta (standard SEK)" %}
|
||||
</summary>
|
||||
<div class="mt-4 space-y-4">
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-700">{{ empty_form.currency.label }}</label>
|
||||
{{ empty_form.currency }}
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">Använd detta om kvittot är i annan valuta än svenska kronor.</p>
|
||||
<p class="text-xs text-gray-500">{% trans "Använd detta om kvittot är i annan valuta än svenska kronor." %}</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-700">{{ empty_form.receipt.label }}</label>
|
||||
{{ empty_form.receipt }}
|
||||
<p class="mt-1 text-xs text-gray-500">PDF, JPG eller PNG – max 10 MB.</p>
|
||||
<p class="mt-1 text-xs text-gray-500">{% trans "PDF, JPG eller PNG – max 10 MB." %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
{% extends "claims/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Mina utlägg{% endblock %}
|
||||
{% block title %}{% trans "Mina utlägg" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="space-y-6 py-6">
|
||||
<header class="max-w-3xl">
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-brand-600">Översikt</p>
|
||||
<h1 class="text-3xl font-semibold text-gray-900">Mina utlägg</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">Här ser du alla utlägg du skickat in när du varit inloggad, inklusive status, kvitton och loggar.</p>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-brand-600">{% trans "Översikt" %}</p>
|
||||
<h1 class="text-3xl font-semibold text-gray-900">{% trans "Mina utlägg" %}</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">{% trans "Här ser du status för de utlägg du skickat in när du varit inloggad." %}</p>
|
||||
</header>
|
||||
|
||||
{% if claims %}
|
||||
@@ -16,11 +17,11 @@
|
||||
<article class="rounded-3xl bg-white px-6 py-6 shadow-sm ring-1 ring-gray-100">
|
||||
<div class="flex flex-col gap-4 border-b border-gray-100 pb-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<p class="text-xs uppercase tracking-wide text-gray-500">Skickad {{ claim.created_at|date:"Y-m-d H:i" }}</p>
|
||||
<h2 class="mt-1 text-2xl font-semibold text-gray-900">{{ claim.description|default:"Utlägg" }}</h2>
|
||||
<p class="text-xs uppercase tracking-wide text-gray-500">{% trans "Skickad" %} {{ claim.created_at|date:"Y-m-d H:i" }}</p>
|
||||
<h2 class="mt-1 text-2xl font-semibold text-gray-900">{{ claim.description|default:_("Utlägg") }}</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
Belopp: <strong>{{ claim.amount }} {{ claim.currency }}</strong><br>
|
||||
Projekt: {{ claim.project|default:"-" }}
|
||||
{% trans "Belopp" %}: <strong>{{ claim.amount }} {{ claim.currency }}</strong><br>
|
||||
{% trans "Projekt" %}: {{ claim.project|default:"-" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-start gap-2 text-sm lg:items-end">
|
||||
@@ -29,43 +30,43 @@
|
||||
</span>
|
||||
{% if claim.paid_at %}
|
||||
<span class="rounded-full bg-emerald-50 px-3 py-1 text-xs font-semibold text-emerald-800">
|
||||
Betald {{ claim.paid_at|date:"Y-m-d" }}
|
||||
{% trans "Betald" %} {{ claim.paid_at|date:"Y-m-d" }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 grid gap-4 lg:grid-cols-[2fr,1fr]">
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-gray-500">Detaljer</p>
|
||||
<p class="text-sm font-semibold text-gray-500">{% trans "Detaljer" %}</p>
|
||||
<p class="mt-2 whitespace-pre-wrap text-gray-800">{{ claim.description }}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl bg-slate-50 p-4 text-sm text-gray-600">
|
||||
<p class="font-semibold text-gray-800">Kvitto</p>
|
||||
<p class="font-semibold text-gray-800">{% trans "Kvitto" %}</p>
|
||||
{% if claim.receipt %}
|
||||
<a class="mt-2 inline-flex items-center gap-2 text-brand-600 hover:text-brand-700" href="{{ claim.receipt.url }}" target="_blank" rel="noopener">
|
||||
Visa fil
|
||||
{% trans "Visa fil" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<p class="mt-2 text-xs text-gray-400">Inget kvitto bifogat.</p>
|
||||
<p class="mt-2 text-xs text-gray-400">{% trans "Inget kvitto bifogat." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<details class="mt-4 rounded-2xl border border-dashed border-gray-200 bg-gray-50 p-4 text-sm text-gray-700">
|
||||
<summary class="cursor-pointer select-none text-sm font-semibold text-gray-800">Visa logg</summary>
|
||||
<summary class="cursor-pointer select-none text-sm font-semibold text-gray-800">{% trans "Visa logg" %}</summary>
|
||||
<ul class="mt-3 space-y-2 text-sm text-gray-600">
|
||||
{% for log in claim.logs.all %}
|
||||
<li class="rounded-lg bg-white px-3 py-2 shadow-sm">
|
||||
<p class="font-semibold text-gray-900">{{ log.get_action_display }}</p>
|
||||
<p class="text-xs text-gray-500">{{ log.created_at|date:"Y-m-d H:i" }}</p>
|
||||
{% if log.from_status %}
|
||||
<p class="text-xs text-gray-500">Status: {{ log.get_from_status_display }} → {{ log.get_to_status_display }}</p>
|
||||
<p class="text-xs text-gray-500">{% trans "Status" %}: {{ log.get_from_status_display }} → {{ log.get_to_status_display }}</p>
|
||||
{% endif %}
|
||||
{% if log.note %}
|
||||
<p class="mt-1 text-xs text-gray-600">"{{ log.note }}"</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% empty %}
|
||||
<li class="text-xs text-gray-400">Ingen logg än.</li>
|
||||
<li class="text-xs text-gray-400">{% trans "Ingen logg än." %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
@@ -74,8 +75,8 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rounded-3xl border border-dashed border-gray-200 bg-white px-6 py-10 text-center text-gray-500">
|
||||
<p class="text-lg font-semibold text-gray-900">Inga utlägg ännu</p>
|
||||
<p class="mt-2 text-sm">När du skickar in utlägg medan du är inloggad dyker de upp här.</p>
|
||||
<p class="text-lg font-semibold text-gray-900">{% trans "Inga utlägg ännu" %}</p>
|
||||
<p class="mt-2 text-sm">{% trans "Du har inte skickat in några utlägg ännu eller så gjordes de utan inloggning." %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
{% extends "claims/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Skicka utlägg{% endblock %}
|
||||
{% block title %}{% trans "Skicka utlägg" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="py-8">
|
||||
<div class="mx-auto max-w-4xl text-center">
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-brand-600">Utlägg</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold text-gray-900">Skicka in dina kostnader</h1>
|
||||
<p class="mt-3 text-base text-gray-600">
|
||||
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 <code class="rounded bg-gray-100 px-2 py-1 text-xs">?forms=n</code> i URL:en (max {{ max_extra_forms }}).
|
||||
</p>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-brand-600">{% trans "Utlägg" %}</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold text-gray-900">{% trans "Skicka in dina kostnader" %}</h1>
|
||||
</div>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" class="mx-auto mt-10 max-w-4xl space-y-10">
|
||||
@@ -18,9 +15,9 @@
|
||||
|
||||
<div class="rounded-2xl bg-white shadow-sm ring-1 ring-gray-100">
|
||||
<div class="border-b border-gray-100 px-6 py-5">
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">Steg 1</p>
|
||||
<h2 class="text-2xl font-semibold text-gray-900">Dina uppgifter</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">Vi återkommer via dessa kontaktuppgifter och använder kontonumret för utbetalning.</p>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">{% trans "Steg 1" %}</p>
|
||||
<h2 class="text-2xl font-semibold text-gray-900">{% trans "Dina uppgifter" %}</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">{% trans "Vi återkommer via dessa kontaktuppgifter och använder kontonumret för utbetalning." %}</p>
|
||||
</div>
|
||||
<div class="space-y-6 px-6 py-6">
|
||||
{% for field in claimant_form %}
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
{% extends "claims/base.html" %}
|
||||
|
||||
{% block title %}Tack för ditt utlägg{% endblock %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Tack för ditt utlägg" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="flex min-h-[60vh] items-center justify-center py-10">
|
||||
<div class="w-full max-w-lg rounded-3xl bg-white px-8 py-10 text-center shadow-lg ring-1 ring-gray-100">
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-brand-600">Tack!</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold text-gray-900">Utlägget är skickat</h1>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-brand-600">{% trans "Tack!" %}</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold text-gray-900">{% trans "Utlägget är skickat" %}</h1>
|
||||
<p class="mt-3 text-sm text-gray-600">
|
||||
Vi har tagit emot underlaget. Om du har fler kvitton kan du fylla i ett nytt formulär direkt,
|
||||
annars kan du logga in för att följa statusen.
|
||||
{% trans "Vi har tagit emot underlaget. Om du har fler kvitton kan du fylla i ett nytt formulär direkt, annars kan du logga in för att följa statusen." %}
|
||||
</p>
|
||||
<div class="mt-6 flex flex-col gap-3 sm:flex-row sm:justify-center">
|
||||
<a href="{% url 'claims:submit' %}"
|
||||
class="inline-flex items-center justify-center rounded-full bg-brand-600 px-5 py-3 text-sm font-semibold text-white transition hover:bg-brand-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-600 focus-visible:ring-offset-2">
|
||||
Skicka nytt utlägg
|
||||
{% trans "Skicka nytt utlägg" %}
|
||||
</a>
|
||||
<a href="{% url 'login' %}"
|
||||
class="inline-flex items-center justify-center rounded-full border border-gray-200 px-5 py-3 text-sm font-semibold text-gray-700 transition hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-600 focus-visible:ring-offset-2">
|
||||
Logga in
|
||||
{% trans "Logga in" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
{% extends "claims/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Användarhantering{% endblock %}
|
||||
{% block title %}{% trans "Användarhantering" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="space-y-10 py-8">
|
||||
<header class="max-w-3xl">
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-brand-600">Konton & behörigheter</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold text-gray-900">Hantera användare</h1>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-brand-600">{% trans "Konton & behörigheter" %}</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold text-gray-900">{% trans "Hantera användare" %}</h1>
|
||||
<p class="mt-3 text-sm text-gray-600">
|
||||
Skapa nya konton, justera rättigheter för claim-flödet och ta bort användare som inte längre ska ha åtkomst.
|
||||
{% trans "Skapa nya konton, justera rättigheter för claim-flödet och ta bort användare som inte längre ska ha åtkomst." %}
|
||||
</p>
|
||||
<div class="mt-4 rounded-2xl border border-dashed border-amber-200 bg-amber-50 px-4 py-3 text-xs text-amber-800">
|
||||
Notis: denna sida styr direkta behörigheter. Rättigheter via grupper eller superuser-status gäller även om kryssrutorna avmarkeras.
|
||||
{% trans "Notis: denna sida styr direkta behörigheter. Rättigheter via grupper eller superuser-status gäller även om kryssrutorna avmarkeras." %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid gap-8 lg:grid-cols-2">
|
||||
<div class="rounded-3xl bg-white px-6 py-8 shadow-sm ring-1 ring-gray-100">
|
||||
<div class="border-b border-gray-100 pb-4">
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">Nytt konto</p>
|
||||
<h2 class="mt-1 text-2xl font-semibold text-gray-900">Skapa användare</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">Lösenordet valideras mot Djangos standardregler.</p>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">{% trans "Nytt konto" %}</p>
|
||||
<h2 class="mt-1 text-2xl font-semibold text-gray-900">{% trans "Skapa användare" %}</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">{% trans "Lösenordet valideras mot Djangos standardregler." %}</p>
|
||||
</div>
|
||||
<form method="post" class="mt-6 space-y-4">
|
||||
{% csrf_token %}
|
||||
@@ -38,8 +39,6 @@
|
||||
value="{{ field.value|default_if_none:'' }}"
|
||||
class="mt-1 block w-full rounded-xl border border-gray-200 px-3 py-2 text-sm shadow-sm focus:border-brand-600 focus:outline-none focus:ring-2 focus:ring-brand-600"
|
||||
{% if field.field.required %}required{% endif %}
|
||||
{% if field.field.widget.attrs.autocomplete %}autocomplete="{{ field.field.widget.attrs.autocomplete }}"{% endif %}
|
||||
{% if field.field.widget.attrs.autofocus %}autofocus{% endif %}
|
||||
/>
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
@@ -51,33 +50,33 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button type="submit" class="w-full rounded-2xl bg-brand-600 px-4 py-3 text-sm font-semibold text-white transition hover:bg-brand-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-600 focus-visible:ring-offset-2">
|
||||
Skapa användare
|
||||
{% trans "Skapa användare" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="rounded-3xl bg-slate-900 px-6 py-8 text-slate-100 shadow-sm ring-1 ring-slate-800">
|
||||
<h3 class="text-2xl font-semibold">Tips för kontohantering</h3>
|
||||
<ul class="mt-4 space-y-3 text-sm text-slate-300 break-words">
|
||||
<h3 class="text-2xl font-semibold">{% trans "Tips för kontohantering" %}</h3>
|
||||
<ul class="mt-4 space-y-3 text-sm text-slate-300">
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="mt-1 h-2 w-2 rounded-full bg-brand-400"></span>
|
||||
<span class="min-w-0">Lägg användare i grupper via Django admin om flera personer ska dela samma roll.</span>
|
||||
<span class="min-w-0">{% trans "Lägg användare i grupper via Django admin om flera personer ska dela samma roll." %}</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="mt-1 h-2 w-2 rounded-full bg-brand-400"></span>
|
||||
<span class="min-w-0">
|
||||
Behörigheterna <code class="break-normal rounded bg-slate-800 px-2 py-1 text-xs">claims.view_claim</code>
|
||||
{% blocktrans %}Behörigheterna <code class="break-normal rounded bg-slate-800 px-2 py-1 text-xs">claims.view_claim</code>
|
||||
och <code class="break-normal rounded bg-slate-800 px-2 py-1 text-xs">claims.change_claim</code>
|
||||
styr åtkomst till adminvyn respektive beslutsflödet.
|
||||
styr åtkomst till adminvyn respektive beslutsflödet.{% endblocktrans %}
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="mt-1 h-2 w-2 rounded-full bg-brand-400"></span>
|
||||
<span class="min-w-0">En markerad <strong>Admin/staff</strong>-användare kan nå Django admin och skapa projekt, exportflöden m.m.</span>
|
||||
<span class="min-w-0">{% trans "En markerad Admin/staff-användare kan nå Django admin och skapa projekt, exportflöden m.m." %}</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="mt-1 h-2 w-2 rounded-full bg-brand-400"></span>
|
||||
<span class="min-w-0">Ta bara bort konton du är säker på – historik försvinner inte, men personen tappar all åtkomst.</span>
|
||||
<span class="min-w-0">{% trans "Ta bara bort konton du är säker på – historik försvinner inte, men personen tappar all åtkomst." %}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -85,8 +84,8 @@
|
||||
|
||||
<section class="space-y-6">
|
||||
<div>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">Befintliga användare</p>
|
||||
<h2 class="text-2xl font-semibold text-gray-900">Justera behörigheter</h2>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">{% trans "Befintliga användare" %}</p>
|
||||
<h2 class="text-2xl font-semibold text-gray-900">{% trans "Justera behörigheter" %}</h2>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
{% for row in user_rows %}
|
||||
@@ -97,16 +96,14 @@
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-xl font-semibold text-gray-900">{{ user.username }}</h3>
|
||||
{% if user.is_superuser %}
|
||||
<span class="rounded-full bg-emerald-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-emerald-700">Superuser</span>
|
||||
<span class="rounded-full bg-emerald-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-emerald-700">{% trans "Superuser" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ user.get_full_name|default:"Saknar namn" }} · {{ user.email|default:"Ingen e-post" }}
|
||||
{{ user.get_full_name|default:_("Saknar namn") }} · {{ user.email|default:_("Ingen e-post") }}
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-xs uppercase tracking-wide text-gray-400">
|
||||
ID: {{ user.id }}
|
||||
</p>
|
||||
<p class="text-xs uppercase tracking-wide text-gray-400">ID: {{ user.id }}</p>
|
||||
</div>
|
||||
<div class="mt-4 grid gap-6 lg:grid-cols-[2fr,1fr]">
|
||||
<form method="post" class="space-y-4 rounded-2xl bg-slate-50 p-4">
|
||||
@@ -115,51 +112,36 @@
|
||||
{{ form.user_id }}
|
||||
<div class="space-y-3 text-sm text-gray-700">
|
||||
<label class="flex items-center gap-2" for="{{ form.is_staff.id_for_label }}">
|
||||
<input type="checkbox"
|
||||
id="{{ form.is_staff.id_for_label }}"
|
||||
name="{{ form.is_staff.html_name }}"
|
||||
value="on"
|
||||
{% if form.is_staff.value %}checked{% endif %}
|
||||
class="h-4 w-4 rounded border-gray-300 text-brand-600 focus:ring-brand-600">
|
||||
<span>Admin/staff</span>
|
||||
{{ form.is_staff }}
|
||||
<span>{% trans "Admin/staff" %}</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2" for="{{ form.grant_view.id_for_label }}">
|
||||
<input type="checkbox"
|
||||
id="{{ form.grant_view.id_for_label }}"
|
||||
name="{{ form.grant_view.html_name }}"
|
||||
value="on"
|
||||
{% if form.grant_view.value %}checked{% endif %}
|
||||
class="h-4 w-4 rounded border-gray-300 text-brand-600 focus:ring-brand-600">
|
||||
<span>Får se utlägg</span>
|
||||
{{ form.grant_view }}
|
||||
<span>{% trans "Får se utlägg" %}</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2" for="{{ form.grant_change.id_for_label }}">
|
||||
<input type="checkbox"
|
||||
id="{{ form.grant_change.id_for_label }}"
|
||||
name="{{ form.grant_change.html_name }}"
|
||||
value="on"
|
||||
{% if form.grant_change.value %}checked{% endif %}
|
||||
class="h-4 w-4 rounded border-gray-300 text-brand-600 focus:ring-brand-600">
|
||||
<span>Får besluta utlägg</span>
|
||||
{{ form.grant_change }}
|
||||
<span>{% trans "Får besluta utlägg" %}</span>
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="w-full rounded-2xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-brand-700">
|
||||
Spara behörigheter
|
||||
{% trans "Spara behörigheter" %}
|
||||
</button>
|
||||
</form>
|
||||
<div class="rounded-2xl border border-red-100 bg-red-50 p-4 text-sm text-red-800">
|
||||
<p class="font-semibold">Ta bort konto</p>
|
||||
<p class="font-semibold">{% trans "Ta bort konto" %}</p>
|
||||
{% if delete_form %}
|
||||
<p class="mt-1 text-xs text-red-700">Åtgärden går inte att ångra. Användaren förlorar omedelbart åtkomst.</p>
|
||||
<form method="post" class="mt-4" onsubmit="return confirm('Ta bort {{ user.username }}?');">
|
||||
<p class="mt-1 text-xs text-red-700">{% trans "Åtgärden går inte att ångra. Användaren förlorar omedelbart åtkomst." %}</p>
|
||||
<form method="post" class="mt-4" onsubmit="return confirm('{% blocktrans %}Ta bort {{ user.username }}?{% endblocktrans %}');">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="delete">
|
||||
{{ delete_form.user_id }}
|
||||
<button type="submit" class="w-full rounded-full bg-red-600 px-4 py-2 text-xs font-semibold text-white transition hover:bg-red-700">
|
||||
Ta bort användare
|
||||
{% trans "Ta bort användare" %}
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="mt-2 text-xs text-red-700">Kan inte tas bort (antingen du själv eller superuser).</p>
|
||||
<p class="mt-2 text-xs text-red-700">{% trans "Kan inte tas bort (antingen du själv eller superuser)." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -167,8 +149,8 @@
|
||||
{% endwith %}
|
||||
{% empty %}
|
||||
<div class="rounded-3xl border border-dashed border-gray-200 bg-white px-6 py-10 text-center text-gray-500">
|
||||
<p class="text-lg font-semibold text-gray-900">Inga användare ännu</p>
|
||||
<p class="mt-2 text-sm">Skapa det första kontot via formuläret ovan.</p>
|
||||
<p class="text-lg font-semibold text-gray-900">{% trans "Inga användare upplagda." %}</p>
|
||||
<p class="mt-2 text-sm">{% trans "Skapa det första kontot via formuläret ovan." %}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
8
claims/templates/emails/claim_submitted_admin.html
Normal file
8
claims/templates/emails/claim_submitted_admin.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<h2>Nytt utlägg inskickat</h2>
|
||||
<ul>
|
||||
<li><strong>Person:</strong> {{ claim.full_name }} ({{ claim.email }})</li>
|
||||
<li><strong>Belopp:</strong> {{ claim.amount }} {{ claim.currency }}</li>
|
||||
<li><strong>Projekt:</strong> {{ claim.project|default:"-" }}</li>
|
||||
<li><strong>Beskrivning:</strong> {{ claim.description }}</li>
|
||||
</ul>
|
||||
<p>Logga in för att granska och godkänna.</p>
|
||||
9
claims/templates/emails/claim_submitted_claimant.html
Normal file
9
claims/templates/emails/claim_submitted_claimant.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<h2>Hej {{ claim.full_name }},</h2>
|
||||
<p>Tack för att du skickade in ditt utlägg. Vi har mottagit följande information:</p>
|
||||
<ul>
|
||||
<li><strong>Belopp:</strong> {{ claim.amount }} {{ claim.currency }}</li>
|
||||
<li><strong>Projekt:</strong> {{ claim.project|default:"-" }}</li>
|
||||
<li><strong>Beskrivning:</strong> {{ claim.description }}</li>
|
||||
</ul>
|
||||
<p>Du får en ny avisering när ett beslut har fattats.</p>
|
||||
<p>Vänliga hälsningar,<br>claims-system</p>
|
||||
@@ -7,6 +7,7 @@ from django.forms import formset_factory
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views import View
|
||||
from django.views.generic import ListView, TemplateView
|
||||
|
||||
@@ -18,6 +19,7 @@ from .forms import (
|
||||
UserManagementForm,
|
||||
UserPermissionForm,
|
||||
)
|
||||
from .email_utils import notify_admin_of_claim, send_claimant_confirmation_email
|
||||
from .models import Claim, ClaimLog, SystemSetting
|
||||
|
||||
User = get_user_model()
|
||||
@@ -108,15 +110,17 @@ class SubmitClaimView(View):
|
||||
performed_by=claim.submitted_by,
|
||||
to_status=Claim.Status.PENDING,
|
||||
)
|
||||
send_claimant_confirmation_email(claim)
|
||||
notify_admin_of_claim(claim)
|
||||
created += 1
|
||||
|
||||
if created:
|
||||
messages.success(request, f"{created} utlägg skickade in.")
|
||||
messages.success(request, _("{} utlägg skickade in.").format(created))
|
||||
return redirect(reverse("claims:submit-success"))
|
||||
|
||||
messages.error(request, "Inga utlägg kunde sparas. Fyll i minst en rad.")
|
||||
messages.error(request, _("Inga utlägg kunde sparas. Fyll i minst en rad."))
|
||||
else:
|
||||
messages.error(request, "Kunde inte spara utläggen. Kontrollera formuläret.")
|
||||
messages.error(request, _("Kunde inte spara utläggen. Kontrollera formuläret."))
|
||||
|
||||
return render(request, self.template_name, self.build_context(formset, claimant_form))
|
||||
|
||||
@@ -161,7 +165,7 @@ class ClaimAdminListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
|
||||
def _handle_decision(self, request):
|
||||
if not request.user.has_perm("claims.change_claim"):
|
||||
messages.error(request, "Du har inte behörighet att uppdatera utlägg.")
|
||||
messages.error(request, _("Du har inte behörighet att uppdatera utlägg."))
|
||||
return redirect(request.get_full_path())
|
||||
|
||||
form = ClaimDecisionForm(request.POST)
|
||||
@@ -175,17 +179,17 @@ class ClaimAdminListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
action = form.cleaned_data["action"]
|
||||
decision_note = form.cleaned_data.get("decision_note", "")
|
||||
if claim.is_paid:
|
||||
messages.error(request, "Utlägget är redan markerat som betalt och kan inte ändras.")
|
||||
messages.error(request, _("Utlägget är redan markerat som betalt och kan inte ändras."))
|
||||
return redirect(request.get_full_path())
|
||||
previous_status = claim.status
|
||||
claim.decision_note = decision_note
|
||||
|
||||
if action == ClaimDecisionForm.ACTION_APPROVE:
|
||||
claim.status = Claim.Status.APPROVED
|
||||
messages.success(request, f"{claim} markerades som godkänd.")
|
||||
messages.success(request, _("%(claim)s markerades som godkänd.") % {"claim": claim})
|
||||
else:
|
||||
claim.status = Claim.Status.REJECTED
|
||||
messages.warning(request, f"{claim} markerades som nekad.")
|
||||
messages.warning(request, _("%(claim)s markerades som nekad.") % {"claim": claim})
|
||||
|
||||
claim.save(update_fields=["status", "decision_note", "updated_at"])
|
||||
claim.add_log(
|
||||
@@ -199,18 +203,18 @@ class ClaimAdminListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
|
||||
def _handle_payment(self, request):
|
||||
if not SystemSetting.internal_payments_active():
|
||||
messages.error(request, "Betalningshantering är inte aktiverad.")
|
||||
messages.error(request, _("Betalningshantering är inte aktiverad."))
|
||||
return redirect(request.get_full_path())
|
||||
if not request.user.has_perm("claims.change_claim"):
|
||||
messages.error(request, "Du har inte behörighet att uppdatera utlägg.")
|
||||
messages.error(request, _("Du har inte behörighet att uppdatera utlägg."))
|
||||
return redirect(request.get_full_path())
|
||||
|
||||
claim = get_object_or_404(Claim, pk=request.POST.get("payment_claim_id"))
|
||||
if claim.status != Claim.Status.APPROVED:
|
||||
messages.error(request, "Endast godkända utlägg kan markeras som betalda.")
|
||||
messages.error(request, _("Endast godkända utlägg kan markeras som betalda."))
|
||||
return redirect(request.get_full_path())
|
||||
if claim.is_paid:
|
||||
messages.info(request, "Detta utlägg är redan markerat som betalt.")
|
||||
messages.info(request, _("Detta utlägg är redan markerat som betalt."))
|
||||
return redirect(request.get_full_path())
|
||||
|
||||
claim.paid_by = request.user
|
||||
@@ -221,7 +225,7 @@ class ClaimAdminListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
performed_by=request.user,
|
||||
note="Markerad som betald via systemet.",
|
||||
)
|
||||
messages.success(request, f"{claim} markerades som betald.")
|
||||
messages.success(request, _("%(claim)s markerades som betald.") % {"claim": claim})
|
||||
return redirect(request.get_full_path())
|
||||
|
||||
|
||||
@@ -250,7 +254,7 @@ class UserManagementView(LoginRequiredMixin, PermissionRequiredMixin, TemplateVi
|
||||
def _ensure_perm(self, perm_codename):
|
||||
perm = f"auth.{perm_codename}"
|
||||
if not self.request.user.has_perm(perm):
|
||||
messages.error(self.request, "Du saknar behörighet för åtgärden.")
|
||||
messages.error(self.request, _("Du saknar behörighet för åtgärden."))
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -297,7 +301,7 @@ class UserManagementView(LoginRequiredMixin, PermissionRequiredMixin, TemplateVi
|
||||
)
|
||||
self._set_perm(user, "claims.view_claim", form.cleaned_data.get("grant_view", False))
|
||||
self._set_perm(user, "claims.change_claim", form.cleaned_data.get("grant_change", False))
|
||||
messages.success(request, f"Användaren {user.username} skapades.")
|
||||
messages.success(request, _("Användaren %(user)s skapades.") % {"user": user.username})
|
||||
return redirect(request.path)
|
||||
return self.render_to_response(self.get_context_data(create_form=form))
|
||||
|
||||
@@ -308,15 +312,15 @@ class UserManagementView(LoginRequiredMixin, PermissionRequiredMixin, TemplateVi
|
||||
if form.is_valid():
|
||||
user = get_object_or_404(User, pk=form.cleaned_data["user_id"])
|
||||
if user == request.user and not form.cleaned_data["is_staff"]:
|
||||
messages.error(request, "Du kan inte ta bort din egen staff-status.")
|
||||
messages.error(request, _("Du kan inte ta bort din egen staff-status."))
|
||||
return redirect(request.path)
|
||||
user.is_staff = form.cleaned_data["is_staff"]
|
||||
user.save(update_fields=["is_staff"])
|
||||
self._set_perm(user, "claims.view_claim", form.cleaned_data["grant_view"])
|
||||
self._set_perm(user, "claims.change_claim", form.cleaned_data["grant_change"])
|
||||
messages.success(request, f"Behörigheter uppdaterades för {user.username}.")
|
||||
messages.success(request, _("Behörigheter uppdaterades för %(user)s.") % {"user": user.username})
|
||||
else:
|
||||
messages.error(request, "Kunde inte uppdatera behörigheter.")
|
||||
messages.error(request, _("Kunde inte uppdatera behörigheter."))
|
||||
return redirect(request.path)
|
||||
|
||||
elif action == "delete":
|
||||
@@ -326,15 +330,15 @@ class UserManagementView(LoginRequiredMixin, PermissionRequiredMixin, TemplateVi
|
||||
if form.is_valid():
|
||||
user = get_object_or_404(User, pk=form.cleaned_data["user_id"])
|
||||
if user == request.user:
|
||||
messages.error(request, "Du kan inte ta bort ditt eget konto.")
|
||||
messages.error(request, _("Du kan inte ta bort ditt eget konto."))
|
||||
elif user.is_superuser:
|
||||
messages.error(request, "Du kan inte ta bort en superuser via detta gränssnitt.")
|
||||
messages.error(request, _("Du kan inte ta bort en superuser via detta gränssnitt."))
|
||||
else:
|
||||
user.delete()
|
||||
messages.warning(request, "Användaren togs bort.")
|
||||
messages.warning(request, _("Användaren togs bort."))
|
||||
return redirect(request.path)
|
||||
|
||||
messages.error(request, "Okänd åtgärd.")
|
||||
messages.error(request, _("Okänd åtgärd."))
|
||||
return redirect(request.path)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -44,6 +44,7 @@ INSTALLED_APPS = [
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
@@ -104,7 +105,16 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
LANGUAGE_CODE = 'sv'
|
||||
|
||||
LANGUAGES = [
|
||||
('sv', _('Swedish')),
|
||||
('en', _('English')),
|
||||
]
|
||||
|
||||
LOCALE_PATHS = [BASE_DIR / 'locale']
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
@@ -126,6 +136,18 @@ LOGOUT_REDIRECT_URL = '/accounts/login/'
|
||||
os.environ.setdefault("CLAIMS_ENABLE_INTERNAL_PAYMENTS", "true")
|
||||
CLAIMS_ENABLE_INTERNAL_PAYMENTS = os.getenv("CLAIMS_ENABLE_INTERNAL_PAYMENTS", "true").lower() in {"1", "true", "yes"}
|
||||
|
||||
# Email configuration
|
||||
EMAIL_BACKEND = os.getenv("EMAIL_BACKEND", "django.core.mail.backends.console.EmailBackend")
|
||||
EMAIL_HOST = os.getenv("EMAIL_HOST", "")
|
||||
EMAIL_PORT = int(os.getenv("EMAIL_PORT", "587"))
|
||||
EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", "true").lower() in {"1", "true", "yes"}
|
||||
EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER", "")
|
||||
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD", "")
|
||||
|
||||
CLAIMS_EMAIL_ENABLED = os.getenv("CLAIMS_EMAIL_ENABLED", "false").lower() in {"1", "true", "yes"}
|
||||
CLAIMS_EMAIL_FROM = os.getenv("CLAIMS_EMAIL_FROM", "no-reply@claims.local")
|
||||
CLAIMS_ADMIN_NOTIFICATION_EMAIL = os.getenv("CLAIMS_ADMIN_NOTIFICATION_EMAIL", "")
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('claims/', include('claims.urls')),
|
||||
path('accounts/', include('django.contrib.auth.urls')),
|
||||
path('i18n/', include('django.conf.urls.i18n')),
|
||||
path('', RedirectView.as_view(pattern_name='claims:submit', permanent=False)),
|
||||
]
|
||||
|
||||
|
||||
BIN
locale/en/LC_MESSAGES/django.mo
Normal file
BIN
locale/en/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
466
locale/en/LC_MESSAGES/django.po
Normal file
466
locale/en/LC_MESSAGES/django.po
Normal file
@@ -0,0 +1,466 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: claims-system 0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-08 23:12+0100\n"
|
||||
"PO-Revision-Date: 2025-11-08 23:40+0100\n"
|
||||
"Last-Translator: ChatGPT <noreply@example.com>\n"
|
||||
"Language-Team: English\n"
|
||||
"Language: en\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
msgid "Namn"
|
||||
msgstr "Name"
|
||||
|
||||
msgid "E-post"
|
||||
msgstr "Email"
|
||||
|
||||
msgid "Kontonummer"
|
||||
msgstr "Account number"
|
||||
|
||||
msgid "Beskrivning"
|
||||
msgstr "Description"
|
||||
|
||||
msgid "Belopp"
|
||||
msgstr "Amount"
|
||||
|
||||
msgid "Valuta"
|
||||
msgstr "Currency"
|
||||
|
||||
msgid "Evenemang/Projekt"
|
||||
msgstr "Project"
|
||||
|
||||
msgid "Kvitto"
|
||||
msgstr "Receipt"
|
||||
|
||||
msgid "Godkänn"
|
||||
msgstr "Approve"
|
||||
|
||||
msgid "Neka"
|
||||
msgstr "Reject"
|
||||
|
||||
msgid "Kommentar"
|
||||
msgstr "Comment"
|
||||
|
||||
msgid "Kommentar krävs när du nekar ett utlägg."
|
||||
msgstr "A comment is required when you reject an expense."
|
||||
|
||||
msgid "Användarnamn"
|
||||
msgstr "Username"
|
||||
|
||||
msgid "Förnamn"
|
||||
msgstr "First name"
|
||||
|
||||
msgid "Efternamn"
|
||||
msgstr "Last name"
|
||||
|
||||
msgid "Lösenord"
|
||||
msgstr "Password"
|
||||
|
||||
msgid "Bekräfta lösenord"
|
||||
msgstr "Confirm password"
|
||||
|
||||
msgid "Administratör (staff)"
|
||||
msgstr "Administrator (staff)"
|
||||
|
||||
msgid "Ge behörighet att se utlägg"
|
||||
msgstr "Allow viewing claims"
|
||||
|
||||
msgid "Ge behörighet att besluta utlägg"
|
||||
msgstr "Allow deciding claims"
|
||||
|
||||
msgid "Användarnamnet är upptaget."
|
||||
msgstr "That username is already taken."
|
||||
|
||||
msgid "Lösenorden matchar inte."
|
||||
msgstr "Passwords do not match."
|
||||
|
||||
msgid "Admin/staff"
|
||||
msgstr "Admin/staff"
|
||||
|
||||
msgid "Får se utlägg"
|
||||
msgstr "May view claims"
|
||||
|
||||
msgid "Får besluta utlägg"
|
||||
msgstr "May decide claims"
|
||||
|
||||
msgid "Pending"
|
||||
msgstr "Pending"
|
||||
|
||||
msgid "Approved"
|
||||
msgstr "Approved"
|
||||
|
||||
msgid "Rejected"
|
||||
msgstr "Rejected"
|
||||
|
||||
msgid "Swedish krona (SEK)"
|
||||
msgstr "Swedish krona (SEK)"
|
||||
|
||||
msgid "Euro (EUR)"
|
||||
msgstr "Euro (EUR)"
|
||||
|
||||
msgid "US dollar (USD)"
|
||||
msgstr "US dollar (USD)"
|
||||
|
||||
msgid "British pound (GBP)"
|
||||
msgstr "British pound (GBP)"
|
||||
|
||||
msgid "Describe what the reimbursement is for"
|
||||
msgstr "Describe what the reimbursement is for"
|
||||
|
||||
msgid "Submitted"
|
||||
msgstr "Submitted"
|
||||
|
||||
msgid "Status changed"
|
||||
msgstr "Status changed"
|
||||
|
||||
msgid "Marked as paid"
|
||||
msgstr "Marked as paid"
|
||||
|
||||
msgid "Admin – Utlägg"
|
||||
msgstr "Admin – Claims"
|
||||
|
||||
msgid "Översikt"
|
||||
msgstr "Overview"
|
||||
|
||||
msgid "Inkomna utlägg"
|
||||
msgstr "Incoming claims"
|
||||
|
||||
msgid "Filtrera på status, granska kvitton och uppdatera beslut direkt i listan."
|
||||
msgstr "Filter by status, review receipts and update decisions right from the list."
|
||||
|
||||
msgid "Alla"
|
||||
msgstr "All"
|
||||
|
||||
msgid "Skapad"
|
||||
msgstr "Created"
|
||||
|
||||
msgid "Person"
|
||||
msgstr "Person"
|
||||
|
||||
msgid "Konto"
|
||||
msgstr "Account"
|
||||
|
||||
msgid "Inloggad användare"
|
||||
msgstr "Signed-in user"
|
||||
|
||||
msgid "Inskickad av gäst"
|
||||
msgstr "Submitted by guest"
|
||||
|
||||
msgid "Betald"
|
||||
msgstr "Paid"
|
||||
|
||||
msgid "av"
|
||||
msgstr "by"
|
||||
|
||||
msgid "Ej markerad som betald"
|
||||
msgstr "Not marked as paid"
|
||||
|
||||
msgid "Sammanfattning"
|
||||
msgstr "Summary"
|
||||
|
||||
msgid "Markerad som betald"
|
||||
msgstr "Marked as paid"
|
||||
|
||||
msgid "Är du säker på att du har lagt upp betalningen? Markera endast som betald om beloppet skickas till banken."
|
||||
msgstr "Are you sure the payment has been scheduled? Only mark as paid if the amount has been sent to the bank."
|
||||
|
||||
msgid "Betala"
|
||||
msgstr "Mark as paid"
|
||||
|
||||
msgid "Intern betalningshantering är av – markera betalning i ekonomisystemet."
|
||||
msgstr "Internal payment handling is off – mark the payment in your finance system instead."
|
||||
|
||||
msgid "Visa kvitto"
|
||||
msgstr "View receipt"
|
||||
|
||||
msgid "Inget kvitto bifogat"
|
||||
msgstr "No receipt attached"
|
||||
|
||||
msgid "Logg & tidslinje"
|
||||
msgstr "Log & timeline"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
|
||||
msgid "Av"
|
||||
msgstr "By"
|
||||
|
||||
msgid "Ingen logg än."
|
||||
msgstr "No log entries yet."
|
||||
|
||||
msgid "Utlägget är markerat som betalt. Ändringar av beslut/kommentar är låsta."
|
||||
msgstr "The claim is marked as paid. Decision/comments are locked."
|
||||
|
||||
msgid "Åtgärd"
|
||||
msgstr "Action"
|
||||
|
||||
msgid "Uppdatera beslut"
|
||||
msgstr "Update decision"
|
||||
|
||||
msgid "Inga utlägg ännu"
|
||||
msgstr "No claims yet"
|
||||
|
||||
msgid "När formuläret tas emot visas posterna automatiskt här."
|
||||
msgstr "As soon as submissions arrive they will appear here."
|
||||
|
||||
msgid "Claims"
|
||||
msgstr "Claims"
|
||||
|
||||
msgid "claims-system"
|
||||
msgstr "claims-system"
|
||||
|
||||
msgid "Offentlig"
|
||||
msgstr "Public"
|
||||
|
||||
msgid "Skicka utlägg"
|
||||
msgstr "Submit claim"
|
||||
|
||||
msgid "Intern"
|
||||
msgstr "Internal"
|
||||
|
||||
msgid "Utläggslista"
|
||||
msgstr "Claims list"
|
||||
|
||||
msgid "Mina utlägg"
|
||||
msgstr "My claims"
|
||||
|
||||
msgid "Användare"
|
||||
msgstr "Users"
|
||||
|
||||
msgid "Export"
|
||||
msgstr "Export"
|
||||
|
||||
msgid "Django admin"
|
||||
msgstr "Django admin"
|
||||
|
||||
msgid "Språk"
|
||||
msgstr "Language"
|
||||
|
||||
msgid "Inloggad som"
|
||||
msgstr "Signed in as"
|
||||
|
||||
msgid "Logga ut"
|
||||
msgstr "Sign out"
|
||||
|
||||
msgid "Logga in"
|
||||
msgstr "Sign in"
|
||||
|
||||
msgid "Export till redovisningssystem"
|
||||
msgstr "Export to bookkeeping system"
|
||||
|
||||
msgid "Detta är ett framtida steg. Här kommer du att kunna:"
|
||||
msgstr "This is a future step. Here you will be able to:"
|
||||
|
||||
msgid "Välja tidsperiod eller status"
|
||||
msgstr "Choose a time range or status"
|
||||
|
||||
msgid "Exportera till t.ex. bankfil eller SIE"
|
||||
msgstr "Export to e.g. bank file or SIE"
|
||||
|
||||
msgid "Skicka data via API till externa system"
|
||||
msgstr "Send data via API to external systems"
|
||||
|
||||
msgid "Planerade åtgärder:"
|
||||
msgstr "Planned actions:"
|
||||
|
||||
msgid "Definiera format"
|
||||
msgstr "Define formats"
|
||||
|
||||
msgid "Implementera exportkommando/API"
|
||||
msgstr "Implement export command/API"
|
||||
|
||||
msgid "Bygga integrationsinställningar"
|
||||
msgstr "Build integration settings"
|
||||
|
||||
msgid "Tills vidare kan du ladda ner data via Django admin eller med ett enkelt SQL-utdrag."
|
||||
msgstr "Until then, download data via Django admin or a simple SQL query."
|
||||
|
||||
msgid "Steg 2"
|
||||
msgstr "Step 2"
|
||||
|
||||
msgid "Utläggsrader"
|
||||
msgstr "Expense rows"
|
||||
|
||||
msgid "Lägg till ett block per kvitto eller kostnad. Projektväljaren hjälper ekonomin att bokföra rätt."
|
||||
msgstr "Add one block per receipt or cost. The project selector helps accounting post it correctly."
|
||||
|
||||
msgid "Totalt <span data-current-count>%(current_forms)s</span> rader"
|
||||
msgstr "Total <span data-current-count>%(current_forms)s</span> rows"
|
||||
|
||||
msgid "justera"
|
||||
msgstr "adjust"
|
||||
|
||||
msgid "Utlägg %(forloop.counter)s"
|
||||
msgstr "Expense %(forloop.counter)s"
|
||||
|
||||
msgid "Obligatoriska fält markeras med *"
|
||||
msgstr "Required fields are marked with *"
|
||||
|
||||
msgid "Avancerat: justera valuta (standard SEK)"
|
||||
msgstr "Advanced: adjust currency (default SEK)"
|
||||
|
||||
msgid "Använd detta om kvittot är i annan valuta än svenska kronor."
|
||||
msgstr "Use this if the receipt uses another currency than SEK."
|
||||
|
||||
msgid "När du skickar in skickas du vidare mot adminvyn. Saknar du inloggning får du möjlighet att logga in."
|
||||
msgstr "After submitting you are redirected to the admin view. If you lack an account you'll be prompted to sign in."
|
||||
|
||||
msgid "Skicka in utlägg"
|
||||
msgstr "Submit claims"
|
||||
|
||||
msgid "Ny utläggsrad"
|
||||
msgstr "New expense row"
|
||||
|
||||
msgid "PDF, JPG eller PNG – max 10 MB."
|
||||
msgstr "PDF, JPG or PNG – max 10 MB."
|
||||
|
||||
msgid "Utlägg"
|
||||
msgstr "Expenses"
|
||||
|
||||
msgid "Skicka in dina kostnader"
|
||||
msgstr "Submit your costs"
|
||||
|
||||
msgid "Formuläret fungerar både för inloggade och gäster. Varje rad nedan motsvarar ett utlägg.\n Behöver du fler rader? Lägg till <code class=\"rounded bg-gray-100 px-2 py-1 text-xs\">?forms=n</code> i URL:en (max {{ max_extra_forms }})."
|
||||
msgstr "The form works for both guests and signed-in users. Each section equals one claim. Need more rows? Append <code class=\"rounded bg-gray-100 px-2 py-1 text-xs\">?forms=n</code> to the URL (max {{ max_extra_forms }})."
|
||||
|
||||
msgid "Steg 1"
|
||||
msgstr "Step 1"
|
||||
|
||||
msgid "Dina uppgifter"
|
||||
msgstr "Your details"
|
||||
|
||||
msgid "Vi återkommer via dessa kontaktuppgifter och använder kontonumret för utbetalning."
|
||||
msgstr "We'll reach you via these details and use the account number for payout."
|
||||
|
||||
msgid "Tack för ditt utlägg"
|
||||
msgstr "Thank you for your claim"
|
||||
|
||||
msgid "Tack!"
|
||||
msgstr "Thanks!"
|
||||
|
||||
msgid "Utlägget är skickat"
|
||||
msgstr "Your claim has been sent"
|
||||
|
||||
msgid "Vi har tagit emot underlaget. Om du har fler kvitton kan du fylla i ett nytt formulär direkt, annars kan du logga in för att följa statusen."
|
||||
msgstr "We have received the submission. Add another receipt right away or log in to follow the status."
|
||||
|
||||
msgid "Skicka nytt utlägg"
|
||||
msgstr "Submit another claim"
|
||||
|
||||
msgid "Konton & behörigheter"
|
||||
msgstr "Accounts & permissions"
|
||||
|
||||
msgid "Hantera användare"
|
||||
msgstr "Manage users"
|
||||
|
||||
msgid "Skapa nya konton, justera rättigheter för claim-flödet och ta bort användare som inte längre ska ha åtkomst."
|
||||
msgstr "Create new accounts, adjust claim permissions and remove users who no longer need access."
|
||||
|
||||
msgid "Notis: denna sida styr direkta behörigheter. Rättigheter via grupper eller superuser-status gäller även om kryssrutorna avmarkeras."
|
||||
msgstr "Note: this page only controls direct permissions. Group or superuser permissions still apply even if boxes are unchecked."
|
||||
|
||||
msgid "Nytt konto"
|
||||
msgstr "New account"
|
||||
|
||||
msgid "Lösenordet valideras mot Djangos standardregler."
|
||||
msgstr "Password is validated against Django's default rules."
|
||||
|
||||
msgid "Tips för kontohantering"
|
||||
msgstr "Account management tips"
|
||||
|
||||
msgid "Lägg användare i grupper via Django admin om flera personer ska dela samma roll."
|
||||
msgstr "Use Django admin groups when multiple people share a role."
|
||||
|
||||
msgid "Behörigheterna <code class=\"break-normal rounded bg-slate-800 px-2 py-1 text-xs\">claims.view_claim</code>\n och <code class=\"break-normal rounded bg-slate-800 px-2 py-1 text-xs\">claims.change_claim</code>\n styr åtkomst till adminvyn respektive beslutsflödet."
|
||||
msgstr "The permissions <code class=\"break-normal rounded bg-slate-800 px-2 py-1 text-xs\">claims.view_claim</code> and <code class=\"break-normal rounded bg-slate-800 px-2 py-1 text-xs\">claims.change_claim</code> control access to the list and decision flows."
|
||||
|
||||
msgid "En markerad Admin/staff-användare kan nå Django admin och skapa projekt, exportflöden m.m."
|
||||
msgstr "Users flagged as Admin/staff may access Django admin to create projects, exports, etc."
|
||||
|
||||
msgid "Ta bara bort konton du är säker på – historik försvinner inte, men personen tappar all åtkomst."
|
||||
msgstr "Only delete accounts you are sure about – history stays, but the person loses access."
|
||||
|
||||
msgid "Befintliga användare"
|
||||
msgstr "Existing users"
|
||||
|
||||
msgid "Justera behörigheter"
|
||||
msgstr "Adjust permissions"
|
||||
|
||||
msgid "Superuser"
|
||||
msgstr "Superuser"
|
||||
|
||||
msgid "Saknar namn"
|
||||
msgstr "No name"
|
||||
|
||||
msgid "Ingen e-post"
|
||||
msgstr "No email"
|
||||
|
||||
msgid "Spara behörigheter"
|
||||
msgstr "Save permissions"
|
||||
|
||||
msgid "Ta bort konto"
|
||||
msgstr "Remove account"
|
||||
|
||||
msgid "Åtgärden går inte att ångra. Användaren förlorar omedelbart åtkomst."
|
||||
msgstr "This action cannot be undone. The user loses access immediately."
|
||||
|
||||
msgid "Ta bort {{ user.username }}?"
|
||||
msgstr "Remove {{ user.username }}?"
|
||||
|
||||
msgid "Ta bort användare"
|
||||
msgstr "Delete user"
|
||||
|
||||
msgid "Kan inte tas bort (antingen du själv eller superuser)."
|
||||
msgstr "Cannot be removed (either yourself or a superuser)."
|
||||
|
||||
msgid "Inga användare upplagda."
|
||||
msgstr "No users yet."
|
||||
|
||||
msgid "Skapa det första kontot via formuläret ovan."
|
||||
msgstr "Create the first account using the form above."
|
||||
|
||||
msgid "Skickad"
|
||||
msgstr "Submitted"
|
||||
|
||||
msgid "Detaljer"
|
||||
msgstr "Details"
|
||||
|
||||
msgid "Visa fil"
|
||||
msgstr "View file"
|
||||
|
||||
msgid "Inget kvitto bifogat."
|
||||
msgstr "No receipt attached."
|
||||
|
||||
msgid "Visa logg"
|
||||
msgstr "Show log"
|
||||
|
||||
msgid "Du har inte skickat in några utlägg ännu eller så gjordes de utan inloggning."
|
||||
msgstr "You haven't submitted any claims yet or they were sent without signing in."
|
||||
|
||||
msgid "Du är utloggad"
|
||||
msgstr "You are signed out"
|
||||
|
||||
msgid "Vi ses snart igen"
|
||||
msgstr "See you soon"
|
||||
|
||||
msgid "Din session är avslutad. Du kan när som helst logga in igen för att hantera utlägg eller administrera systemet."
|
||||
msgstr "Your session has ended. Sign in again anytime to manage claims or administer the system."
|
||||
|
||||
msgid "Till inloggningen"
|
||||
msgstr "Back to login"
|
||||
|
||||
msgid "Välkommen tillbaka"
|
||||
msgstr "Welcome back"
|
||||
|
||||
msgid "Använd dina administratörsuppgifter för att hantera utlägg."
|
||||
msgstr "Use your admin credentials to manage claims."
|
||||
|
||||
msgid "Behöver du ett konto? Kontakta en superuser i organisationen."
|
||||
msgstr "Need an account? Contact a superuser in your organization."
|
||||
|
||||
msgid "Skapa användare"
|
||||
msgstr "Create user"
|
||||
@@ -1,18 +1,19 @@
|
||||
{% extends "claims/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Utloggad{% endblock %}
|
||||
{% block title %}{% trans "Utloggad" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="flex min-h-[50vh] items-center justify-center py-10">
|
||||
<div class="w-full max-w-md rounded-3xl bg-white px-8 py-10 text-center shadow-xl ring-1 ring-gray-100">
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-brand-600">Du är utloggad</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold text-gray-900">Vi ses snart igen</h1>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-brand-600">{% trans "Du är utloggad" %}</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold text-gray-900">{% trans "Vi ses snart igen" %}</h1>
|
||||
<p class="mt-3 text-sm text-gray-600">
|
||||
Din session är avslutad. Du kan när som helst logga in igen för att hantera utlägg eller administrera systemet.
|
||||
{% trans "Din session är avslutad. Du kan när som helst logga in igen för att hantera utlägg eller administrera systemet." %}
|
||||
</p>
|
||||
<a href="{% url 'login' %}"
|
||||
class="mt-6 inline-flex items-center justify-center rounded-full bg-brand-600 px-5 py-3 text-sm font-semibold text-white transition hover:bg-brand-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-600 focus-visible:ring-offset-2">
|
||||
Till inloggningen
|
||||
{% trans "Till inloggningen" %}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
{% extends "claims/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Logga in{% endblock %}
|
||||
{% block title %}{% trans "Logga in" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="flex min-h-[60vh] items-center justify-center py-12">
|
||||
<div class="w-full max-w-md rounded-3xl bg-white px-8 py-10 shadow-xl ring-1 ring-gray-100">
|
||||
<div class="text-center">
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-brand-600">Välkommen tillbaka</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold text-gray-900">Logga in</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">Använd dina administratörsuppgifter för att hantera utlägg.</p>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-brand-600">{% trans "Välkommen tillbaka" %}</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold text-gray-900">{% trans "Logga in" %}</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">{% trans "Använd dina administratörsuppgifter för att hantera utlägg." %}</p>
|
||||
</div>
|
||||
<form method="post" class="mt-8 space-y-6">
|
||||
{% csrf_token %}
|
||||
@@ -39,10 +40,10 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<button type="submit" class="w-full rounded-2xl bg-brand-600 px-4 py-3 text-sm font-semibold text-white transition hover:bg-brand-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-600 focus-visible:ring-offset-2">
|
||||
Logga in
|
||||
{% trans "Logga in" %}
|
||||
</button>
|
||||
</form>
|
||||
<p class="mt-6 text-center text-xs text-gray-400">Behöver du ett konto? Kontakta en superuser i organisationen.</p>
|
||||
<p class="mt-6 text-center text-xs text-gray-400">{% trans "Behöver du ett konto? Kontakta en superuser i organisationen." %}</p>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user