Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions kompassi-v2-frontend/src/__generated__/gql.ts

Large diffs are not rendered by default.

25 changes: 15 additions & 10 deletions kompassi-v2-frontend/src/__generated__/graphql.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ const query = graphql(`
title
quantity
price
vatPercentage
}
paymentStamps {
...AdminOrderPaymentStamp
Expand Down Expand Up @@ -542,6 +543,7 @@ export default async function AdminOrderPage(props: Props) {
<AccordionBody>
<ProductsTable
order={order}
locale={locale}
messages={translations.Tickets}
compact
className="m-0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default async function OrderPage(props: Props) {
<ViewContainer>
<OrderHeader order={order} messages={t} locale={locale} event={event} />

<ProductsTable order={order} messages={t} />
<ProductsTable order={order} locale={locale} messages={t} />

{showPayButton && (
<Section>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import SubmitButton from "@/components/forms/SubmitButton";
import ModalButton from "@/components/ModalButton";
import TicketsAdminView from "@/components/tickets/TicketsAdminView";
import formatMoney from "@/helpers/formatMoney";
import formatVatRate from "@/helpers/formatVatRate";
import getPageTitle from "@/helpers/getPageTitle";
import { getTranslations } from "@/translations";

Expand All @@ -29,6 +30,7 @@ graphql(`
title
description
price
vatPercentage
eticketsPerProduct
maxPerOrder
}
Expand All @@ -41,6 +43,7 @@ graphql(`
title
description
price
vatPercentage
eticketsPerProduct
maxPerOrder
availableFrom
Expand Down Expand Up @@ -183,6 +186,17 @@ export default async function AdminProductDetailPage(props: Props) {
decimalPlaces: 2,
...t.clientAttributes.unitPrice,
},
{
slug: "vatPercentage",
type: "SingleSelect",
choices: [
{ slug: "0.00", title: "0%" },
{ slug: "10.00", title: "10%" },
{ slug: "13.50", title: "13.5%" },
{ slug: "25.50", title: "25.5%" },
],
...t.clientAttributes.vatPercentage,
},
{
slug: "eticketsPerProduct",
type: "NumberField",
Expand Down Expand Up @@ -294,6 +308,15 @@ export default async function AdminProductDetailPage(props: Props) {
getCellContents: (product) => formatMoney(product.price),
className: "col-1 align-middle",
},
{
slug: "vatPercentage",
title: t.clientAttributes.vatPercentage.title,
getCellContents: (product) =>
t.serverAttributes.vatIncluded(
formatVatRate(product.vatPercentage, locale),
),
className: "col-1 align-middle",
},
];

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,17 @@ export default async function ProductsPage(props: Props) {
decimalPlaces: 2,
...t.clientAttributes.unitPrice,
},
{
slug: "vatPercentage",
type: "SingleSelect",
choices: [
{ slug: "0.00", title: "0%" },
{ slug: "10.00", title: "10%" },
{ slug: "13.50", title: "13.5%" },
{ slug: "25.50", title: "25.5%" },
],
...t.clientAttributes.vatPercentage,
},
{
slug: "quota",
type: "NumberField",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export default async function TicketsPage(props: Props) {
<ProductCard
key={product.id}
product={product}
locale={locale}
messages={producT}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const query = graphql(`
title
quantity
price
vatPercentage
}

event {
Expand Down Expand Up @@ -122,7 +123,11 @@ export default async function ProfileOrderPage(props: Props) {
event={order.event}
/>

<ProductsTable order={order} messages={translations.Tickets} />
<ProductsTable
order={order}
locale={locale}
messages={translations.Tickets}
/>

{order.canPay && (
<form action={payOrder.bind(null, locale, eventSlug, orderId)}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ Current approach is as follows:

3. Download the SVG
4. Using eg. OpenInNewTab.tsx as a template, make the SVG into a React component

- You may need to tweak the vertical translate to make the icon align with text.

The `.material-symbol` class lives in `globals.scss`.
Expand Down
14 changes: 13 additions & 1 deletion kompassi-v2-frontend/src/components/tickets/ProductCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@ import Card from "react-bootstrap/Card";
import CardBody from "react-bootstrap/CardBody";
import CardTitle from "react-bootstrap/CardTitle";
import formatMoney from "@/helpers/formatMoney";
import formatVatRate from "@/helpers/formatVatRate";
import { Product } from "@/services/tickets";
import { Translations } from "@/translations/en";

interface Props {
product: Product;
locale: string;
messages: Translations["Tickets"]["Product"];
children?: ReactNode;
}

export default function ProductCard({ product, messages: t, children }: Props) {
export default function ProductCard({
product,
locale,
messages: t,
children,
}: Props) {
const className = product.available ? "" : "text-muted";
return (
<Card key={product.id} className="mb-3">
Expand All @@ -25,6 +32,11 @@ export default function ProductCard({ product, messages: t, children }: Props) {

<div className={`col-md m-md-0 mb-3 fs-4 text-md-end`}>
{formatMoney(product.price)}
<div className="text-muted fs-6">
{t.serverAttributes.vatIncluded(
formatVatRate(product.vatPercentage, locale),
)}
</div>
</div>

<div className={`col-md fs-4`}>
Expand Down
35 changes: 35 additions & 0 deletions kompassi-v2-frontend/src/components/tickets/ProductsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Column, DataTable } from "../DataTable";
import formatMoney from "@/helpers/formatMoney";
import formatVatRate from "@/helpers/formatVatRate";
import type { Translations } from "@/translations/en";

interface Product {
title: string;
quantity: number;
price: string;
vatPercentage: string;
}

interface Order {
Expand All @@ -15,13 +17,33 @@ interface Order {

interface Props {
order: Order;
locale: string;
messages: Translations["Tickets"];
className?: string;
compact?: boolean;
}

function computeVatBreakdown(
products: Product[],
): { rate: string; vat: string }[] {
const totals = new Map<string, number>();
for (const p of products) {
const gross = parseFloat(p.price) * p.quantity;
const prev = totals.get(p.vatPercentage) ?? 0;
totals.set(p.vatPercentage, prev + gross);
}
return Array.from(totals.entries())
.sort(([a], [b]) => parseFloat(a) - parseFloat(b))
.map(([rate, gross]) => {
const r = parseFloat(rate);
const vat = (gross * r) / (100 + r);
return { rate, vat: vat.toFixed(2) };
});
}

export default function ProductsTable({
order,
locale,
messages: t,
className,
compact,
Expand Down Expand Up @@ -63,6 +85,8 @@ export default function ProductsTable({

className = "table table-striped " + (className ?? "mb-5");

const vatBreakdown = computeVatBreakdown(order.products);

return (
<DataTable className={className} rows={order.products} columns={columns}>
<tfoot>
Expand All @@ -75,6 +99,17 @@ export default function ProductsTable({
<strong>{formatMoney(order.totalPrice)}</strong>
</td>
</tr>
{vatBreakdown.map(({ rate, vat }) => (
<tr key={rate} className="text-muted">
<td className="col-8 small">
{t.Product.serverAttributes.vatIncluded(
formatVatRate(rate, locale),
)}
</td>
<td className="col text-end"></td>
<td className="col text-end small">{formatMoney(vat)}</td>
</tr>
))}
</tfoot>
</DataTable>
);
Expand Down
8 changes: 8 additions & 0 deletions kompassi-v2-frontend/src/helpers/formatVatRate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function formatVatRate(value: string, locale: string = "en") {
const num = parseFloat(value);
const formatted = num.toString();
if (locale === "fi" || locale === "sv") {
return formatted.replace(".", ",");
}
Comment on lines +3 to +6
Copy link
Copy Markdown

@hannesj hannesj Apr 24, 2026

Choose a reason for hiding this comment

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

What about using new Intl.NumberFormat(locale).format(num)?

return formatted;
}
2 changes: 2 additions & 0 deletions kompassi-v2-frontend/src/services/tickets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface Product {
title: string;
description: string;
price: string;
vatPercentage: string;
maxPerOrder: number;
available?: boolean;
}
Expand Down Expand Up @@ -169,6 +170,7 @@ export interface Order {
title: string;
price: string;
quantity: number;
vatPercentage: string;
}[];
}

Expand Down
8 changes: 7 additions & 1 deletion kompassi-v2-frontend/src/translations/en.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { JSX, ReactNode } from "react";

const translations = {
Common: {
ok: "OK",
Expand Down Expand Up @@ -531,6 +530,12 @@ const translations = {
selectedQuotas: "Selected quotas",
soldOut: "Sold out",
isAvailable: "Availability schedule",
vatPercentage: {
title: "VAT rate",
helpText:
"The VAT rate that applies to this product. Prices are VAT-inclusive.",
},
vatBreakdown: "VAT breakdown",
dragToReorder: "Drag to reorder",
newProductQuota: {
title: "Quota",
Expand All @@ -539,6 +544,7 @@ const translations = {
},
},
serverAttributes: {
vatIncluded: (rate: string) => `incl. ${rate}% VAT`,
isAvailable: {
untilFurtherNotice: "Available until further notice",
untilTime: (formattedTime: string) =>
Expand Down
7 changes: 7 additions & 0 deletions kompassi-v2-frontend/src/translations/fi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,12 @@ const translations: Translations = {
selectedQuotas: "Valitut kiintiöt",
soldOut: "Loppuunmyyty",
isAvailable: "Saatavuusaika",
vatPercentage: {
title: "ALV-prosentti",
helpText:
"Tähän tuotteeseen sovellettava arvonlisäveroprosentti. Hinnat sisältävät ALV:n.",
},
vatBreakdown: "ALV-erittely",
dragToReorder: "Vedä ja pudota järjestääksesi tuotteita",
newProductQuota: {
title: "Kiintiö",
Expand All @@ -534,6 +540,7 @@ const translations: Translations = {
},
},
serverAttributes: {
vatIncluded: (rate: string) => `sis. ${rate}% ALV`,
isAvailable: {
untilFurtherNotice: "Saatavilla toistaiseksi",
untilTime: (formattedTime: string) =>
Expand Down
7 changes: 7 additions & 0 deletions kompassi-v2-frontend/src/translations/sv.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
// Translators: Kirsi Västi, Calle Tengman, Luka Pajukanta, Claude Sonnet 4.6

import { ReactNode, JSX } from "react";
import en, { Translations } from "./en";

Check warning on line 4 in kompassi-v2-frontend/src/translations/sv.tsx

View workflow job for this annotation

GitHub Actions / eslint

'en' is defined but never used

/// Mark untranslated English strings with this
/// Eg.
/// { foo: UNTRANSLATED("bar") }
function UNTRANSLATED<T>(wat: T): T {

Check warning on line 9 in kompassi-v2-frontend/src/translations/sv.tsx

View workflow job for this annotation

GitHub Actions / eslint

'UNTRANSLATED' is defined but never used
return wat;
}

/// Mark strings to be checked by a native speaker / more experienced translator with this
function UNSURE<T>(wat: T): T {

Check warning on line 14 in kompassi-v2-frontend/src/translations/sv.tsx

View workflow job for this annotation

GitHub Actions / eslint

'UNSURE' is defined but never used
return wat;
}

Expand Down Expand Up @@ -518,6 +518,12 @@
selectedQuotas: "Valda kvoter",
soldOut: "Slutsåld",
isAvailable: "Tillgänglighetsschema",
vatPercentage: {
title: "Momssats",
helpText:
"Den momssats som gäller för denna produkt. Priserna inkluderar moms.",
},
vatBreakdown: "Momsspecifikation",
dragToReorder: "Dra för att sortera om",
newProductQuota: {
title: "Kvot",
Expand All @@ -526,6 +532,7 @@
},
},
serverAttributes: {
vatIncluded: (rate: string) => `inkl. ${rate}% moms`,
isAvailable: {
untilFurtherNotice: "Tillgänglig tills vidare",
untilTime: (formattedTime: string) =>
Expand Down
4 changes: 3 additions & 1 deletion kompassi/tickets_v2/graphql/mutations/create_product.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from kompassi.access.cbac import graphql_check_model
from kompassi.core.models import Event
from kompassi.core.utils.form_utils import camel_case_keys_to_snake_case
from kompassi.event_log_v2.utils.emit import emit

from ...models.product import Product
Expand All @@ -28,6 +29,7 @@ class Meta:
"title",
"description",
"price",
"vat_percentage",
]


Expand All @@ -48,7 +50,7 @@ def mutate(
event = Event.objects.get(slug=input.event_slug)
graphql_check_model(Product, event.scope, info, operation="create")

form = CreateProductForm(data=input.form_data) # type: ignore
form = CreateProductForm(data=camel_case_keys_to_snake_case(input.form_data)) # type: ignore
if not form.is_valid():
raise ValueError(form.errors)

Expand Down
1 change: 1 addition & 0 deletions kompassi/tickets_v2/graphql/mutations/update_product.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class Meta:
"title",
"description",
"price",
"vat_percentage",
"max_per_order",
"etickets_per_product",
"available_from",
Expand Down
1 change: 1 addition & 0 deletions kompassi/tickets_v2/graphql/product_full.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Meta:
"title",
"description",
"price",
"vat_percentage",
"available_from",
"available_until",
"max_per_order",
Expand Down
1 change: 1 addition & 0 deletions kompassi/tickets_v2/graphql/product_limited.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class Meta:
"title",
"description",
"price",
"vat_percentage",
"available_from",
"available_until",
"max_per_order",
Expand Down
Loading
Loading