many updates :-)

This commit is contained in:
Gerhard Scheikl
2026-05-08 10:40:19 +02:00
parent 5b2aa5d62b
commit 770c6fd16a
16 changed files with 876 additions and 151 deletions
+161 -35
View File
@@ -7,15 +7,25 @@ import db from "../db.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { session } = await authenticate.admin(request);
const [settings, recent] = await Promise.all([
const [settings, recent, counts] = await Promise.all([
db.shopSettings.findUnique({ where: { shopDomain: session.shop } }),
db.invoice.findMany({
where: { shopDomain: session.shop },
orderBy: [{ issuedAt: "desc" }],
take: 10,
take: 8,
}),
db.invoice.groupBy({
by: ["status"],
where: { shopDomain: session.shop },
_count: { _all: true },
}),
]);
const total = counts.reduce((acc, row) => acc + row._count._all, 0);
const issuedCount = counts.find((c) => c.status === "issued")?._count._all ?? 0;
const sentCount = counts.find((c) => c.status === "sent")?._count._all ?? 0;
const cancelledCount = counts.find((c) => c.status === "cancelled")?._count._all ?? 0;
const settingsConfigured = !!(
settings &&
settings.companyName &&
@@ -25,12 +35,14 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
return {
settingsConfigured,
metrics: { total, issuedCount, sentCount, cancelledCount },
recent: recent.map((i) => ({
id: i.id,
number: i.invoiceNumber,
kind: i.kind,
orderName: i.orderName,
version: i.version,
status: i.status,
sentAt: i.sentAt?.toISOString() ?? null,
cancelledAt: i.cancelledAt?.toISOString() ?? null,
issuedAt: i.issuedAt.toISOString(),
@@ -39,54 +51,168 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
};
};
const dateFmt = new Intl.DateTimeFormat("en-GB", {
day: "2-digit",
month: "short",
year: "numeric",
});
function formatDate(iso: string | null): string {
if (!iso) return "—";
return dateFmt.format(new Date(iso));
}
interface RecentInvoice {
id: string;
number: string;
kind: string;
orderName: string;
version: number;
status: string;
sentAt: string | null;
cancelledAt: string | null;
issuedAt: string;
pdfUrl: string;
}
function statusBadge(invoice: RecentInvoice): {
tone: "success" | "info" | "critical" | "warning";
label: string;
} {
if (invoice.cancelledAt) return { tone: "critical", label: "Cancelled" };
if (invoice.kind === "storno") return { tone: "warning", label: "Storno" };
if (invoice.sentAt) return { tone: "success", label: "Sent" };
return { tone: "info", label: "Issued" };
}
export default function Index() {
const { settingsConfigured, recent } = useLoaderData<typeof loader>();
const { settingsConfigured, metrics, recent } = useLoaderData<typeof loader>();
return (
<s-page heading="LinumIQ Invoice">
{!settingsConfigured && (
<s-banner tone="warning" heading="Configure your invoice settings">
Complete your company, bank and numbering details so generated
invoices are legally compliant.{" "}
<Link to="/app/settings">Open settings</Link>
<s-paragraph>
Complete your company, bank and numbering details so generated
invoices are legally compliant.
</s-paragraph>
<s-stack direction="inline" gap="base">
<s-link href="/app/settings">Open settings </s-link>
</s-stack>
</s-banner>
)}
<s-section heading="What this app does">
<s-paragraph>
Generates Austrian-compliant PDF invoices for your Shopify orders.
Trigger from the order page (Generate invoice action), via Shopify
Flow, or in bulk from the Invoices page. PDFs are stored on
Shopify Files and linked to each order via metafields.
</s-paragraph>
<s-section heading="Overview">
<s-grid gridTemplateColumns="repeat(4, minmax(0, 1fr))" gap="base">
<Metric label="Total invoices" value={metrics.total} />
<Metric label="Issued" value={metrics.issuedCount} tone="info" />
<Metric label="Sent" value={metrics.sentCount} tone="success" />
<Metric label="Cancelled" value={metrics.cancelledCount} tone="critical" />
</s-grid>
</s-section>
<s-section heading="Recent invoices">
<s-section
heading="Recent invoices"
padding="none"
accessibilityLabel="Recent invoices table"
>
{recent.length === 0 ? (
<s-paragraph>No invoices generated yet.</s-paragraph>
<s-box padding="base">
<s-stack direction="block" gap="base" alignItems="center">
<s-text type="strong">No invoices yet</s-text>
<s-paragraph tone="neutral">
Generate your first invoice from the Invoices page or directly
from a Shopify order.
</s-paragraph>
<s-link href="/app/invoices">Open invoices </s-link>
</s-stack>
</s-box>
) : (
<s-unordered-list>
{recent.map((i) => (
<s-list-item key={i.id}>
{i.kind === "storno" ? "Storno " : ""}
{i.number} order {i.orderName} (v{i.version})
{i.cancelledAt
? " — cancelled"
: i.sentAt
? " — sent"
: ""}
{i.pdfUrl ? (
<>
{" "}
[<a href={i.pdfUrl} target="_blank" rel="noreferrer">PDF</a>]
</>
) : null}
</s-list-item>
))}
</s-unordered-list>
<s-table>
<s-table-header-row>
<s-table-header listSlot="primary">Invoice</s-table-header>
<s-table-header>Order</s-table-header>
<s-table-header>Issued</s-table-header>
<s-table-header listSlot="secondary">Status</s-table-header>
<s-table-header listSlot="labeled">PDF</s-table-header>
</s-table-header-row>
<s-table-body>
{recent.map((invoice) => {
const badge = statusBadge(invoice);
return (
<s-table-row key={invoice.id}>
<s-table-cell>
<s-stack direction="block" gap="none">
<s-text type="strong">{invoice.number}</s-text>
{invoice.version > 1 ? (
<s-text tone="neutral">v{invoice.version}</s-text>
) : null}
</s-stack>
</s-table-cell>
<s-table-cell>{invoice.orderName}</s-table-cell>
<s-table-cell>{formatDate(invoice.issuedAt)}</s-table-cell>
<s-table-cell>
<s-badge tone={badge.tone}>{badge.label}</s-badge>
</s-table-cell>
<s-table-cell>
<s-stack direction="inline" gap="small" justifyContent="end">
{invoice.pdfUrl ? (
<s-link href={invoice.pdfUrl} target="_blank">
Open
</s-link>
) : (
<s-text tone="neutral"></s-text>
)}
</s-stack>
</s-table-cell>
</s-table-row>
);
})}
</s-table-body>
</s-table>
)}
<Link to="/app/invoices">Open invoices page</Link>
<s-box padding="base">
<s-stack direction="inline" justifyContent="end">
<Link to="/app/invoices">View all invoices </Link>
</s-stack>
</s-box>
</s-section>
<s-section heading="How it works">
<s-stack direction="block" gap="base">
<s-paragraph>
LinumIQ Invoice generates Austrian-compliant PDF invoices for your
Shopify orders. PDFs are stored on Shopify Files and linked to
each order via metafields.
</s-paragraph>
<s-paragraph tone="neutral">
Trigger generation from the order page (Generate invoice action),
via Shopify Flow, or in bulk from the Invoices page.
</s-paragraph>
</s-stack>
</s-section>
</s-page>
);
}
function Metric({
label,
value,
tone,
}: {
label: string;
value: number;
tone?: "info" | "success" | "critical";
}) {
const valueTone = tone ?? "neutral";
return (
<s-box padding="base" background="subdued" border="base" borderRadius="base">
<s-stack direction="block" gap="small">
<s-text tone="neutral">{label}</s-text>
<s-heading>
<s-text tone={valueTone}>{value}</s-text>
</s-heading>
</s-stack>
</s-box>
);
}