From 844acaa5737344ba66fe48d61f2ecf959f01d753 Mon Sep 17 00:00:00 2001 From: Nico Schett <52858351+schettn@users.noreply.github.com> Date: Fri, 24 May 2024 15:25:27 +0000 Subject: [PATCH] feat: replace kassabuch with registers upload --- src/components/ui/dropzone.tsx | 103 +++ src/pages/kassabuch/[registerId].tsx | 1067 -------------------------- src/pages/kassabuch/index.tsx | 792 ------------------- src/pages/registers.tsx | 265 +++++++ 4 files changed, 368 insertions(+), 1859 deletions(-) create mode 100644 src/components/ui/dropzone.tsx delete mode 100644 src/pages/kassabuch/[registerId].tsx delete mode 100644 src/pages/kassabuch/index.tsx create mode 100644 src/pages/registers.tsx diff --git a/src/components/ui/dropzone.tsx b/src/components/ui/dropzone.tsx new file mode 100644 index 0000000..2d2f841 --- /dev/null +++ b/src/components/ui/dropzone.tsx @@ -0,0 +1,103 @@ +import React, { useRef, useState } from "react"; +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; + +// Define the props expected by the Dropzone component +interface DropzoneProps { + onChange: React.Dispatch>; + className?: string; + fileExtension?: string; +} + +// Create the Dropzone component receiving props +export function Dropzone({ + onChange, + className, + fileExtension, + ...props +}: DropzoneProps) { + // Initialize state variables using the useState hook + const fileInputRef = useRef(null); // Reference to file input element + const [fileInfo, setFileInfo] = useState(null); // Information about the uploaded file + const [error, setError] = useState(null); // Error message state + + // Function to handle drag over event + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }; + + // Function to handle drop event + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + const { files } = e.dataTransfer; + handleFiles(files); + }; + + // Function to handle file input change event + const handleFileInputChange = (e: React.ChangeEvent) => { + const { files } = e.target; + if (files) { + handleFiles(files); + } + }; + + // Function to handle processing of uploaded files + const handleFiles = (files: FileList) => { + const uploadedFile = files[0]; + + // Check file extension + if (fileExtension && !uploadedFile.name.endsWith(`.${fileExtension}`)) { + setError(`Invalid file type. Expected: .${fileExtension}`); + return; + } + + const fileSizeInKB = Math.round(uploadedFile.size / 1024); // Convert to KB + + const fileList = Array.from(files).map((file) => file); + onChange((prevFiles) => [...prevFiles, ...fileList]); + + // Display file information + setFileInfo(`Uploaded file: ${uploadedFile.name} (${fileSizeInKB} KB)`); + setError(null); // Reset error state + }; + + // Function to simulate a click on the file input element + const handleButtonClick = () => { + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }; + + return ( + + +
+ + Drag Files to Upload or click here + + + +
+ {fileInfo &&

{fileInfo}

} + {error && {error}} +
+
+ ); +} diff --git a/src/pages/kassabuch/[registerId].tsx b/src/pages/kassabuch/[registerId].tsx deleted file mode 100644 index 920fe5a..0000000 --- a/src/pages/kassabuch/[registerId].tsx +++ /dev/null @@ -1,1067 +0,0 @@ -import * as React from "react"; - -import { PageConfig, PageProps } from "@atsnek/jaen"; - -import { ReloadIcon } from "@radix-ui/react-icons"; -import { - ColumnDef, - ColumnFiltersState, - SortingState, - VisibilityState, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; -import { - ArrowUpDown, - CalendarIcon, - ChevronDown, - MoreHorizontal, - PlusIcon, -} from "lucide-react"; - -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { - Table, - TableBody, - TableCaption, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; - -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; -import { - HoverCard, - HoverCardContent, - HoverCardTrigger, -} from "@/components/ui/hover-card"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { sq } from "@/pylons/kassabuch"; -import { - PaymentMethodType, - PaymentMethodTypeInput, - RegisterType, - SortOrderInput, - Tax, - TaxType, - TaxTypeInput, - Transaction, -} from "@/pylons/kassabuch/src/schema.generated"; -import { Link } from "gatsby"; -import { useLazyQuery } from "snek-query/react-hooks"; -import { zodResolver } from "@hookform/resolvers/zod"; - -type TransactionType = { - timestamp: string; - taxes: { - type: TaxType; - total: number; - net: number; - tax: number; - }[]; - products: { - name: string; - amount: number; - total: number; - }[]; - paymentMethods: { - type: PaymentMethodType; - total: number; - }[]; - revenue: number; -}; - -function getMonthNameFromIndex(monthIndex: number): string { - const date = new Date(); - date.setMonth(monthIndex); - return date.toLocaleDateString(undefined, { month: "long" }); -} - -interface ManuelCreateTransactionButtonProps { - registerId: number; - onCreated: () => void; -} - -const ManuelCreateTransactionSchema = z.object({ - timestamp: z.date(), - taxes: z.array( - z.object({ - type: z.nativeEnum(TaxTypeInput), - total: z.number(), - }) - ), - paymentMethods: z.array( - z.object({ - type: z.nativeEnum(PaymentMethodTypeInput), - total: z.number(), - }) - ), - revenue: z.number(), -}); - -const ManuelCreateTransactionButton: React.FC< - ManuelCreateTransactionButtonProps -> = ({ registerId, onCreated }) => { - const [open, setOpen] = React.useState(false); - - const form = useForm>({ - resolver: zodResolver(ManuelCreateTransactionSchema), - defaultValues: { - timestamp: new Date(), - taxes: [ - { type: TaxTypeInput.MWST_10, total: 0 }, - { type: TaxTypeInput.MWST_20, total: 0 }, - ], - paymentMethods: [ - { type: PaymentMethodTypeInput.CASH, total: 0 }, - { type: PaymentMethodTypeInput.CARD, total: 0 }, - ], - }, - }); - - React.useEffect(() => { - if (form.formState.isSubmitSuccessful) { - form.reset(); - setOpen(false); - } - }, [form.formState.isSubmitSuccessful, form.reset]); - - async function onSubmit(data: z.infer) { - console.log(data); - - try { - const [_, errors] = await sq.mutate((m) => - m.addEndOfDayTransaction({ - registerId: registerId, - timestamp: data.timestamp.toISOString(), - taxes: data.taxes.map((tax) => ({ - type: asEnumKey(TaxTypeInput, tax.type), - total: tax.total, - })), - paymentMethods: data.paymentMethods.map((paymentMethod) => ({ - type: asEnumKey(PaymentMethodTypeInput, paymentMethod.type), - total: paymentMethod.total, - })), - revenue: data.revenue, - }) - ); - - if (errors) { - throw new Error(errors[0].message); - } - - toast({ - title: "Transaction created", - description: "Die Transaktion wurde erfolgreich erstellt.", - }); - - onCreated(); - } catch (error) { - toast({ - title: "Transaction creation failed", - description: error.message, - variant: "destructive", - }); - } - } - - return ( - - - - - - Manuelle Eingabe - - Füge eine Transaktion manuell hinzu. Dies kann nicht rückgängig - gemacht werden. - - -
- - ( - - Datum - - - - - - - - - date > new Date() || date < new Date("1900-01-01") - } - initialFocus - /> - - - - Wähle das Datum der Transaktion. - - - - )} - /> - - ( - - Steuern 10% - - - field.onChange({ - type: TaxType.MWST_10, - total: parseFloat(event.target.value), - }) - } - /> - - Füge die Steuern 10% hinzu. - - - )} - /> - - ( - - Steuern 20% - - - field.onChange({ - type: TaxType.MWST_20, - total: parseFloat(event.target.value), - }) - } - /> - - Füge die Steuern 20% hinzu. - - - )} - /> - - ( - - Bar - - - field.onChange({ - type: PaymentMethodType.CASH, - total: parseFloat(event.target.value), - }) - } - /> - - Füge Barzahlungen hinzu. - - - )} - /> - - ( - - Karte - - - field.onChange({ - type: PaymentMethodType.CARD, - total: parseFloat(event.target.value), - }) - } - /> - - Füge Kartenzahlungen hinzu. - - - )} - /> - - ( - - Umsatz - - - field.onChange(parseFloat(event.target.value)) - } - /> - - Füge den Umsatz hinzu. - - - )} - /> - - - - - - -
-
- ); -}; - -export const columns: ColumnDef[] = [ - { - accessorKey: "timestamp", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const timestamp = new Date(row.getValue("timestamp")); - const formattedDatetime = timestamp.toLocaleDateString("de-AT", { - weekday: "long", - year: "numeric", - month: "long", - day: "numeric", - hour: "numeric", - minute: "numeric", - second: "numeric", - }); - - return
{formattedDatetime}
; - }, - }, - { - accessorKey: "products", - header: "Products", - cell: ({ row }) => { - const products = row.getValue("products") as TransactionType["products"]; - - return ( - - - {" "} -

- {products.map((product) => product.name).join(", ")} - - {" "} - ({products.length}) - -

-
- - - Umsatz aus Produkten - - - Produkt - Menge - Umsatz - - - - {products.map((product) => ( - - {product.name} - {product.amount} - - {(product.total || 0).toLocaleString("de-DE", { - style: "currency", - currency: "EUR", - })} - - - ))} - -
-
-
- ); - - return ( -
- {products.map((product) => ( -
-
{product.name}
-
{product.amount}
-
{product.total}
-
- ))} -
- ); - }, - }, - { - accessorKey: "revenue", - header: "Revenue", - cell: ({ row }) => { - const revenue = row.getValue("revenue") || 0; - - return ( -

- {revenue.toLocaleString("de-DE", { - style: "currency", - currency: "EUR", - })} -

- ); - }, - }, - { - accessorKey: "taxes", - header: "Taxes", - cell: ({ row }) => { - const taxes = row.getValue("taxes") as TransactionType["taxes"]; - - return ( - - - {taxes.map((tax) => ( -
-

- - {tax.type === TaxType.MWST_10 - ? "10%" - : TaxType.MWST_20 - ? "20%" - : "UNKOWN"} - : - - - {(tax.total || 0).toLocaleString("de-DE", { - style: "currency", - currency: "EUR", - })} -

-
- ))} -
- - - Umsatz aus Steuern - - - Steuersatz - Netto - Steuer - Brutto - - - - {taxes.map((tax) => ( - - - {tax.type === TaxType.MWST_10 - ? "10%" - : TaxType.MWST_20 - ? "20%" - : "UNKOWN"} - - - {(tax.net || 0).toLocaleString("de-DE", { - style: "currency", - currency: "EUR", - })} - - - {(tax.tax || 0).toLocaleString("de-DE", { - style: "currency", - currency: "EUR", - })} - - - {(tax.total || 0).toLocaleString("de-DE", { - style: "currency", - currency: "EUR", - })} - - - ))} - -
-
-
- ); - }, - }, - { - accessorKey: "paymentMethods", - header: "Payment Methods", - cell: ({ row }) => { - const paymentMethods = row.getValue( - "paymentMethods" - ) as TransactionType["paymentMethods"]; - - return ( -
- {paymentMethods.map((paymentMethod) => ( -
-

- {paymentMethod.type}: - {(paymentMethod.total || 0).toLocaleString("de-DE", { - style: "currency", - currency: "EUR", - })} -

-
- ))} -
- ); - }, - }, - - { - id: "actions", - enableHiding: false, - cell: ({ row }) => { - const payment = row.original; - - return ( - - - - - - Actions - navigator.clipboard.writeText(payment.id)} - > - Copy payment ID - - - View customer - View payment details - - - ); - }, - }, -]; - -export const DataTableDemo: React.FC<{ registerId: number }> = (props) => { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [] - ); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [rowSelection, setRowSelection] = React.useState({}); - - const [_, { data, refetch }] = useLazyQuery(sq); - - const [year, setYear] = React.useState(null); - const [month, setMonth] = React.useState(null); - - React.useEffect(() => { - // Set to current year and month - - setYear(new Date().getFullYear()); - setMonth(new Date().getMonth()); - }, []); - - React.useEffect(() => { - if (!props.registerId) return; - - refetch(); - }, [props.registerId, year, month]); - - const transactions = React.useMemo(() => { - const nodes = data.me - .register({ where: { id: props.registerId } }) - .transactions({ - where: { - endOfDayTransaction: { is: {} }, - timestamp: - year && month - ? { - gte: new Date(year, month, 1).toISOString(), - lte: new Date(year, month + 1, 0).toISOString(), - } - : {}, - }, - orderBy: [{ timestamp: asEnumKey(SortOrderInput, "asc") }], - }).nodes; - - return nodes.map((transaction: Transaction) => { - const taxes = transaction - .endOfDayTransaction()! - .taxes() - .nodes.map((tax: Tax) => ({ - type: tax.type, - total: tax.total, - net: tax.net, - tax: tax.tax, - })); - - const products = transaction - .endOfDayTransaction()! - .products() - .nodes.map((product) => ({ - name: product.name, - amount: product.amount, - total: product.total, - })); - - const paymentMethods = transaction - .endOfDayTransaction()! - .paymentMethods() - .nodes.map((paymentMethod) => ({ - type: paymentMethod.type, - total: paymentMethod.total, - })); - - return { - timestamp: transaction.timestamp, - taxes, - products, - paymentMethods, - revenue: transaction.endOfDayTransaction()!.revenue, - }; - }); - }, [data, year, month]); - - const table = useReactTable({ - data: transactions, - columns, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onColumnVisibilityChange: setColumnVisibility, - onRowSelectionChange: setRowSelection, - state: { - sorting, - columnFilters, - columnVisibility, - rowSelection, - }, - }); - - return ( -
-
- {/* - table.getColumn("email")?.setFilterValue(event.target.value) - } - className="max-w-sm" - /> */} - -
- - - -
-
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - )) - ) : ( - - - No results. - - - )} - -
-
-
-
- {table.getFilteredSelectedRowModel().rows.length} of{" "} - {table.getFilteredRowModel().rows.length} row(s) selected. -
-
- - -
-
-
- ); -}; - -import { toast } from "@/components/ui/use-toast"; -import { cn } from "@/lib/utils"; -import { pylonURL } from "@/pylons/kassabuch/src"; -import { User } from "oidc-client-ts"; -import { z } from "zod"; -import { useForm } from "react-hook-form"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTrigger, -} from "@/components/ui/dialog"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { Calendar } from "@/components/ui/calendar"; -import { format } from "date-fns"; -import { Input } from "@/components/ui/input"; -import { asEnumKey } from "snek-query"; - -const JournalUploadButton: React.FC<{ - onUpload: () => void; - registerId: number; -}> = ({ onUpload, registerId }) => { - const [isUploading, setIsUploading] = React.useState(false); - - const inputRef = React.useRef(null); - - const handleFileChange = async ( - event: React.ChangeEvent - ) => { - const file = event.target.files?.[0]; - - if (!file) return; - - const oidcStorage = sessionStorage.getItem( - `oidc.user:${__JAEN_ZITADEL__.authority}:${__JAEN_ZITADEL__.clientId}` - ); - - if (oidcStorage) { - const user = User.fromStorageString(oidcStorage); - - setIsUploading(true); - - const formData = new FormData(); - formData.append("file", file); - - const res = await fetch(`${pylonURL}/register/${registerId}/upload`, { - method: "POST", - headers: { - Authorization: `Bearer ${user.access_token}`, - }, - body: formData, - }); - - setIsUploading(false); - - if (res.ok) { - toast({ - title: "Journal uploaded", - description: "Das Journal wurde erfolgreich hochgeladen.", - }); - - onUpload(); - } else { - toast({ - title: "Journal upload failed", - description: "Das Journal konnte nicht hochgeladen werden.", - variant: "destructive", - }); - } - } - }; - - return ( - - ); -}; - -const Page: React.FC = (props) => { - const registerId = parseInt(props.params.registerId); - - const [_, { data, errors, refetch }] = useLazyQuery(sq); - - const { name: registerName, type: registerType } = data.me.register({ - where: { id: registerId }, - }); - - React.useEffect(() => { - refetch(); - }, [registerId]); - - if (errors) { - console.error(errors); - - return
Something went wrong.
; - } - - return ( -
- - - - - Kassabuch - - - - - - {registerName} - - - - -
-

{registerName}

- - {registerType === RegisterType.MANUEL ? ( - - ) : ( - - )} -
- - -
- ); -}; - -export default Page; - -export { Head } from "@atsnek/jaen"; - -export const pageConfig: PageConfig = { - label: "Kassabuch", - layout: { - name: "jaen", - }, - auth: { - isRequired: true, - roles: ["260237544631828483:kassabuch:admin"], - }, -}; diff --git a/src/pages/kassabuch/index.tsx b/src/pages/kassabuch/index.tsx deleted file mode 100644 index e28b29d..0000000 --- a/src/pages/kassabuch/index.tsx +++ /dev/null @@ -1,792 +0,0 @@ -import * as React from "react"; - -import { PageConfig, PageProps } from "@atsnek/jaen"; - -import { - ColumnDef, - ColumnFiltersState, - SortingState, - VisibilityState, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; -import { - CalendarIcon, - ChevronDown, - MoreHorizontal, - PlusIcon, -} from "lucide-react"; -import { useLazyQuery, useQuery } from "snek-query/react-hooks"; - -import "chartjs-adapter-date-fns"; // Import Date-fns adapter for Chart.js -import { Line } from "react-chartjs-2"; - -import { - Legend, - LineController, - LineElement, - LinearScale, - PointElement, - TimeScale, - Tooltip, -} from "chart.js"; - -import { ReloadIcon } from "@radix-ui/react-icons"; - -Chart.register( - LineElement, - LineController, - LinearScale, - PointElement, - Tooltip, - Legend, - TimeScale -); - -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; - -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbList, - BreadcrumbPage, -} from "@/components/ui/breadcrumb"; -import { sq } from "@/pylons/kassabuch"; -import { - RegisterType, - RegisterTypeInput, - SortOrderInput, -} from "@/pylons/kassabuch/src/schema.generated"; -import { Chart } from "chart.js"; -import { addDays, format } from "date-fns"; - -import { Calendar } from "@/components/ui/calendar"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { cn } from "@/lib/utils"; -import { DateRange } from "react-day-picker"; -import { asEnumKey } from "snek-query"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Link } from "gatsby"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { z } from "zod"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; -import { FaCashRegister } from "@react-icons/all-files/fa/FaCashRegister"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; -import { Label } from "@/components/ui/label"; -import { toast } from "@/components/ui/use-toast"; -import { User } from "oidc-client-ts"; -import { pylonURL } from "@/pylons/kassabuch/src"; - -const getColorForId = (id: number) => { - const hue = (id * 137.508) % 360; // Generate hue based on the id - return `hsl(${hue}, 70%, 50%)`; // Convert hue to RGB color -}; - -const Graph = () => { - const [_, { data, refetch }] = useLazyQuery(sq); - - const [date, setDate] = React.useState({ - from: new Date(new Date().getFullYear(), 0, 1), - to: new Date(), - }); - - React.useEffect(() => { - refetch(); - }, [date]); - - const graphData = React.useMemo(() => { - const registers = data.me.registers().nodes; - - return registers.map((register) => { - const transactions = register.transactions({ - where: { - endOfDayTransaction: { is: {} }, - timestamp: date - ? { - gte: date.from?.toISOString(), - lte: date.to?.toISOString(), - } - : {}, - }, - orderBy: [{ timestamp: asEnumKey(SortOrderInput, "asc") }], - }).nodes; - - return { - label: register.name, // Assign a label for the register - data: transactions.map((transaction) => ({ - x: new Date(transaction.timestamp), - y: transaction.endOfDayTransaction()!.revenue, - })), - borderColor: getColorForId(register.id), // Assign a color based on the register id - tension: 0.1, - fill: true, - }; - }); - }, [data, date]); - - const chartData = { - datasets: graphData, - }; - - return ( -
-
-

Umsatzentwicklung nach Kassen

- - - - - - - - - { - // set date + 1 day to include the selected date - setDate({ - from: date?.from, - to: date?.to ? addDays(date.to, 1) : undefined, - }); - }} - numberOfMonths={2} - /> - - - - -
- - -
- ); -}; - -type RegisterType = { - id: number; - type: RegisterType; - name: string; - totalNetRevenue: string; -}; - -export const columns: ColumnDef[] = [ - { - accessorKey: "name", - header: "Name", - cell: ({ row }) =>
{row.getValue("name")}
, - }, - { - accessorKey: "type", - header: "Type", - cell: ({ row }) => { - const type = row.getValue("type"); - - return ( -
- {type === RegisterType.SHARP && "Sharp"} - {type === RegisterType.QMP_18 && "Casio QMP-18"} - {type === RegisterType.MANUEL && "Manuelle Kassa"} -
- ); - }, - }, - { - accessorKey: "totalNetRevenue", - header: () =>
Net Revenue
, - cell: ({ row }) => { - const amount = parseFloat(row.getValue("totalNetRevenue")); - - // Format the amount as a dollar amount - const formatted = new Intl.NumberFormat("de-AT", { - style: "currency", - currency: "EUR", - }).format(amount); - - return
{formatted}
; - }, - }, - { - id: "actions", - enableHiding: false, - meta: { - style: { - textAlign: "end", - }, - }, - cell: ({ row, onDelete }) => { - const register = row.original; - - return ( - - - - - - Actions - - - Öffnen - - - { - const oidcStorage = sessionStorage.getItem( - `oidc.user:${__JAEN_ZITADEL__.authority}:${__JAEN_ZITADEL__.clientId}` - ); - - if (oidcStorage) { - const user = User.fromStorageString(oidcStorage); - - const res = await fetch( - `${pylonURL}/register/${register.id}/export`, - { - headers: { - Authorization: `Bearer ${user.access_token}`, - }, - } - ); - - let fileName = res.headers - .get("Content-Disposition") - ?.split("filename=")[1]; - - // Cut off the quotes - fileName = fileName?.substring(1, fileName.length - 1); - - // This returns a csv file that should be downloaded - const blob = await res.blob(); - - const url = window.URL.createObjectURL(blob); - - const a = document.createElement("a"); - a.href = url; - a.download = fileName || `register-${register.id}.csv`; - a.click(); - - window.URL.revokeObjectURL(url); - - toast({ - title: "Success", - description: "Erfolgreich exportiert.", - }); - } - }} - > - Exportieren als CSV - - - - - e.preventDefault()}> - - Löschen - - - - - - Bist du dir sicher? - - Diese Aktion kann nicht rückgängig gemacht werden. Die Daten - werden unwiderruflich gelöscht. - - - - Abbrechen - { - await sq.mutate((m) => - m.registerDelete({ id: register.id }) - ); - - onDelete(); - }} - > - Löschen - - - - - - - ); - }, - }, -]; - -const RegisterCreateSchema = z.object({ - name: z.string(), - type: z.nativeEnum(RegisterType), -}); - -const RegisterCreateButton: React.FC<{ - onCreate?: () => void; -}> = (props) => { - const [open, setOpen] = React.useState(false); - - const form = useForm>({ - resolver: zodResolver(RegisterCreateSchema), - }); - - React.useEffect(() => { - if (form.formState.isSubmitSuccessful) { - form.reset(); - setOpen(false); - } - }, [form.formState.isSubmitSuccessful, form.reset]); - - async function onSubmit(data: z.infer) { - console.log(data); - - try { - const [_, errors] = await sq.mutate((m) => - m.registerCreate({ - name: data.name, - type: asEnumKey(RegisterTypeInput, data.type), - }) - ); - - if (errors) { - throw new Error(errors[0].message); - } - - toast({ - title: "Success", - description: "Kassa wurde erfolgreich erstellt.", - }); - - props.onCreate?.(); - } catch (error) { - toast({ - title: "Error", - variant: "destructive", - description: error.message, - }); - } - } - - return ( - - - - - - - Kassa erstellen - {/* - This action cannot be undone. This will permanently delete your - account and remove your data from our servers. - */} - - -
- - ( - - Name - - - - - - )} - /> - - ( - - Kassatyp - - - - - - - Sharp - - - - - - - Casio QMP-18 - - - - - - - - Manuelle Kassa - - - - - - - )} - /> - - - - - - -
-
- ); -}; - -export function DataTableDemo() { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [] - ); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [rowSelection, setRowSelection] = React.useState({}); - - const { data, refetch } = useQuery(sq); - - const tabelData = React.useMemo(() => { - return data.me.registers().nodes.map((register) => ({ - id: register.id, - type: register.type, - name: register.name, - totalNetRevenue: register.totalNetRevenue, - })); - }, [data]); - - const table = useReactTable({ - data: tabelData, - columns, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onColumnVisibilityChange: setColumnVisibility, - onRowSelectionChange: setRowSelection, - state: { - sorting, - columnFilters, - columnVisibility, - rowSelection, - }, - }); - - return ( -
-
- -
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {cell.column.id !== "actions" ? ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ) : ( - flexRender(cell.column.columnDef.cell, { - ...cell.getContext(), - onDelete: () => { - refetch(); - }, - }) - )} - - ))} - - )) - ) : ( - - - No results. - - - )} - -
-
-
-
- - -
-
-
- ); -} - -const Page: React.FC = () => { - return ( -
- - - - Kassabuch - - - - - - - -
- ); -}; - -export default Page; - -export { Head } from "@atsnek/jaen"; - -export const pageConfig: PageConfig = { - label: "Kassabuch", - icon: "FaCashRegister", - layout: { - name: "jaen", - }, - menu: { - type: "app", - label: "Kassabuch", - }, - auth: { - isRequired: true, - roles: ["260237544631828483:kassabuch:admin"], - }, -}; diff --git a/src/pages/registers.tsx b/src/pages/registers.tsx new file mode 100644 index 0000000..7e3edee --- /dev/null +++ b/src/pages/registers.tsx @@ -0,0 +1,265 @@ +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Dropzone } from "@/components/ui/dropzone"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { toast } from "@/components/ui/use-toast"; +import { cn } from "@/lib/utils"; +import { pylonURL } from "@/pylons/kassabuch/src"; +import { PageConfig, PageProps } from "@atsnek/jaen"; +import { ReloadIcon } from "@radix-ui/react-icons"; +import { DeleteIcon } from "lucide-react"; +import { useState } from "react"; + +const Page: React.FC = () => { + const [files, setFiles] = useState([]); + const [registerNames, setRegisterNames] = useState([]); + const [registerTypes, setRegisterTypes] = useState([]); + + const [isUploading, setIsUploading] = useState(false); + + return ( + + + Upload registers + + + Upload the registers journals you want to convert to a CSV file. You + can upload multiple files at once. + + + + +

Uploaded files

+ + + + + + {files.length} file{files.length === 1 ? "" : "s"} uploaded + + + + Filename + Size + Register name + Register type + + + + + {/* + INV001 + Paid + Credit Card + $250.00 + */} + + {files.map((file, index) => ( + + {file.name} + + {Math.round(file.size / 1024)} KB + + + { + setRegisterNames((values) => { + const newValues = [...values]; + newValues[index] = e.target.value; + return newValues; + }); + }} + /> + + + + + + + + + ))} + +
+
+ + + + +
+ ); +}; + +export default Page; + +export const pageConfig: PageConfig = { + label: "Registers", + icon: "FaCashRegister", + layout: { + name: "jaen", + }, + menu: { + type: "app", + label: "Registers", + }, +};