From 1c7c1265e43f7df2af2e183e43933f76a707537a Mon Sep 17 00:00:00 2001 From: Simon Knittel Date: Sun, 18 Jan 2026 15:09:09 +0100 Subject: [PATCH] feat(silc): Add SILC transaction creation to TopBar Create menu (Vibe Kanban) (#1873) (closes #1511) ## Summary This PR adds the ability to create SILC transactions directly from the TopBar's "Neu" (Create) menu, providing quick access to this commonly used feature. ## Changes Made ### New Component - **`CreateSilcTransactionForm.tsx`**: A new form component that follows the CreateContext pattern with: - Citizen selection input (supports multiple recipients) - Value input (supports negative values to deduct balance) - Optional description field - "Save" and "Save and create another" buttons for efficient bulk creation ### Integration - **`CreateContext.tsx`**: Registered the new form with modal heading "Neue SILC-Transaktion" and 480px width - **`Create.tsx`**: Added authorization check for `silcTransactionOfOtherCitizen:create` permission and menu item "SILC-Transaktion" ### Changelog - Added entry for January 18, 2026 documenting the new feature ## Implementation Details - Uses the existing `createSilcTransaction` server action - Follows the same patterns as other create forms (CreateTaskForm, CreateCitizenForm, etc.) - Authorization uses `silcTransactionOfOtherCitizen` permission, consistent with the existing SILC transaction creation in other parts of the app - Items in the Create menu are sorted alphabetically --- This PR was written using [Vibe Kanban](https://vibekanban.com) --- app/src/app/app/changelog/page.tsx | 12 ++ .../common/components/CreateContext.tsx | 11 ++ .../shell/components/TopBar/Create.tsx | 16 ++- .../components/CreateSilcTransactionForm.tsx | 106 ++++++++++++++++++ 4 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 app/src/modules/silc/components/CreateSilcTransactionForm.tsx diff --git a/app/src/app/app/changelog/page.tsx b/app/src/app/app/changelog/page.tsx index cb1bcc026..18ac69009 100644 --- a/app/src/app/app/changelog/page.tsx +++ b/app/src/app/app/changelog/page.tsx @@ -44,6 +44,18 @@ export default async function Page() { return (
+ + +

+ Du kannst jetzt SILC-Transaktionen direkt über den “Neu”-Button in der + TopBar erstellen. +

+
+
+ ).then((mod) => mod.CreateProfitDistributionCycleForm), ); +const CreateSilcTransactionForm = dynamic(() => + import( + "@/modules/silc/components/CreateSilcTransactionForm" + ).then((mod) => mod.CreateSilcTransactionForm), +); + export const createForms = { citizen: { formComponent: CreateCitizenForm, @@ -80,6 +86,11 @@ export const createForms = { modalHeading: "Neuer Task", modalWidth: "w-[768px]", }, + silcTransaction: { + formComponent: CreateSilcTransactionForm, + modalHeading: "Neue SILC-Transaktion", + modalWidth: "w-[480px]", + }, }; interface CreateContext { diff --git a/app/src/modules/shell/components/TopBar/Create.tsx b/app/src/modules/shell/components/TopBar/Create.tsx index 56affff5c..e31ecdb55 100644 --- a/app/src/modules/shell/components/TopBar/Create.tsx +++ b/app/src/modules/shell/components/TopBar/Create.tsx @@ -39,6 +39,10 @@ export const Create = ({ className }: Props) => { const showCreateTask = Boolean( authentication && authentication.authorize("task", "create"), ); + const showCreateSilcTransaction = Boolean( + authentication && + authentication.authorize("silcTransactionOfOtherCitizen", "create"), + ); if ( !showCreateCitizen && @@ -46,7 +50,8 @@ export const Create = ({ className }: Props) => { !showCreateOrganization && !showCreateRole && !showCreatePenaltyEntry && - !showCreateTask + !showCreateTask && + !showCreateSilcTransaction ) return null; @@ -73,6 +78,7 @@ export const Create = ({ className }: Props) => { showCreateRole={showCreateRole} showCreatePenaltyEntry={showCreatePenaltyEntry} showCreateTask={showCreateTask} + showCreateSilcTransaction={showCreateSilcTransaction} />
@@ -86,6 +92,7 @@ interface PopoverChildrenProps { readonly showCreateRole: boolean; readonly showCreatePenaltyEntry: boolean; readonly showCreateTask: boolean; + readonly showCreateSilcTransaction: boolean; } const PopoverChildren = ({ @@ -95,6 +102,7 @@ const PopoverChildren = ({ showCreateRole, showCreatePenaltyEntry, showCreateTask, + showCreateSilcTransaction, }: PopoverChildrenProps) => { const { closePopover } = usePopover(); const { openCreateModal } = useCreateContext(); @@ -157,6 +165,12 @@ const PopoverChildren = ({ }); if (showCreateTask) items.push({ label: "Task", type: "button", modalId: "task" }); + if (showCreateSilcTransaction) + items.push({ + label: "SILC-Transaktion", + type: "button", + modalId: "silcTransaction", + }); items = items.toSorted((a, b) => a.label.localeCompare(b.label)); diff --git a/app/src/modules/silc/components/CreateSilcTransactionForm.tsx b/app/src/modules/silc/components/CreateSilcTransactionForm.tsx new file mode 100644 index 000000000..4773d343d --- /dev/null +++ b/app/src/modules/silc/components/CreateSilcTransactionForm.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { CitizenInput } from "@/modules/citizen/components/CitizenInput"; +import Button from "@/modules/common/components/Button"; +import { Button2 } from "@/modules/common/components/Button2"; +import { NumberInput } from "@/modules/common/components/form/NumberInput"; +import { Textarea } from "@/modules/common/components/form/Textarea"; +import Note from "@/modules/common/components/Note"; +import { unstable_rethrow } from "next/navigation"; +import { useActionState } from "react"; +import toast from "react-hot-toast"; +import { FaSave, FaSpinner } from "react-icons/fa"; +import { createSilcTransaction } from "../actions/createSilcTransaction"; + +interface Props { + readonly onSuccess?: () => void; +} + +export const CreateSilcTransactionForm = ({ onSuccess }: Props) => { + const [state, formAction, isPending] = useActionState( + async (previousState: unknown, formData: FormData) => { + try { + const response = await createSilcTransaction(formData); + + if (response.error) { + toast.error(response.error); + console.error(response); + return response; + } + + toast.success(response.success!); + if (formData.has("createAnother")) { + return response; + } + + onSuccess?.(); + return response; + } catch (error) { + unstable_rethrow(error); + toast.error( + "Ein unbekannter Fehler ist aufgetreten. Bitte versuche es später erneut.", + ); + console.error(error); + return { + error: + "Ein unbekannter Fehler ist aufgetreten. Bitte versuche es später erneut.", + requestPayload: formData, + }; + } + }, + null, + ); + + return ( +
+ + + + +