This commit is contained in:
Thomas Peterson 2025-10-05 09:36:25 +02:00
parent 8d10f506d9
commit bc35b70361
41 changed files with 1992 additions and 64736 deletions

View File

@ -3,7 +3,11 @@
namespace PSC\Shop\PaymentBundle\Api;
use Doctrine\ORM\EntityManagerInterface;
use Nelmio\ApiDocBundle\Attribute\Model;
use Nelmio\ApiDocBundle\Attribute\Security;
use OpenApi\Attributes\JsonContent;
use OpenApi\Attributes\Response;
use OpenApi\Attributes\Tag;
use PSC\Shop\EntityBundle\Entity\Payment;
use PSC\Shop\PaymentBundle\Dto\All\Output;
use PSC\Shop\PaymentBundle\Model\Payment as PSCPayment;
@ -11,6 +15,7 @@ use PSC\System\SettingsBundle\Service\Shop;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class All extends AbstractController

View File

@ -33,7 +33,7 @@ class Update extends AbstractController
description: 'update paper',
content: new JsonContent(ref: new Model(type: Paper::class)),
)]
#[RequestBody(ref: new Model(type: Paper::class))]
#[RequestBody(content: new JsonContent(ref: new Model(type: Paper::class)))]
#[Tag(name: 'PaperDB')]
#[Security(name: 'ApiKeyAuth')]
#[Route(path: '/paperdb/update/{artNr}', methods: ['PUT'])]

View File

@ -37,7 +37,7 @@ class Update extends AbstractController
#[Tag(name: 'PaperDB')]
#[Security(name: 'Bearer')]
#[Route(path: '/papercontainer', methods: ['PUT'])]
#[RequestBody(ref: new Model(type: Papercontainer::class))]
#[RequestBody(content: new JsonContent(ref: new Model(type: Papercontainer::class)))]
#[ParamConverter(
'papercontainer',
class: '\PSC\System\SettingsBundle\Model\Papercontainer',

View File

@ -0,0 +1,141 @@
<?php
namespace Plugin\Custom\Hug\Contact\Controller\Backend;
use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ORM\EntityManagerInterface;
use Plugin\Custom\Hug\Contact\Form\UploadType;
use PSC\Shop\ContactBundle\Repository\AddressRepository;
use PSC\Shop\ContactBundle\Repository\ContactRepository;
use PSC\Shop\EntityBundle\Document\Contact as PSCContact;
use PSC\Shop\EntityBundle\Entity\Contact;
use PSC\Shop\EntityBundle\Entity\ContactAddress;
use PSC\System\PluginBundle\Form\Chain\Field;
use PSC\System\SettingsBundle\Service\Log;
use Role;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Annotation\Route;
class StartController extends AbstractController
{
public function __construct(
private readonly ContactRepository $contactRepository,
private readonly EntityManagerInterface $entityManager,
private readonly AddressRepository $contactAddressRepository,
private readonly DocumentManager $documentManager,
) {}
#[Template]
#[Route('/start', name: 'plugin_custom_hug_contact_importer_start')]
public function start(
Request $request,
\PSC\System\SettingsBundle\Service\Shop $shopService,
DocumentManager $documentManager,
EntityManagerInterface $entityManager,
Field $fieldService,
SessionInterface $session,
Log $logService,
) {
$selectedShop = $shopService->getSelectedShop();
$data = [];
$form = $this->createForm(UploadType::class, $data);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
/** @var Role $shop */
$role = $this->entityManager
->getRepository('PSC\Shop\EntityBundle\Entity\Role')
->findOneBy(['level' => 10]);
/** @var UploadedFile $file */
$file = $data['file'];
$import_start_row = $data['import_start_row'];
$import_stop_row = $data['import_stop_row'];
$testAgainstFormats = [
\PhpOffice\PhpSpreadsheet\IOFactory::READER_XLS,
\PhpOffice\PhpSpreadsheet\IOFactory::READER_XLSX,
];
/** Load $inputFileName to a Spreadsheet Object **/
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file->getRealPath(), 0, $testAgainstFormats);
$workSheet = $spreadsheet->getSheet(0);
foreach ($workSheet->getRowIterator($import_start_row, $import_stop_row) as $row) {
if ($workSheet->getCell([3, $row->getRowIndex()])->getValue() == '') {
continue;
}
$contactEntity = $this->contactRepository->getContactByEmailAndShop(
$workSheet->getCell([3, $row->getRowIndex()])->getValue(),
$selectedShop->getId(),
);
if (!$contactEntity) {
$contactEntity = new Contact();
$contactDoc = new \PSC\Shop\EntityBundle\Document\Contact();
$contactInvoiceAddress = new ContactAddress();
$contactDeliveryAddress = new ContactAddress();
} else {
$contactEntity = $contactEntity->getContact();
$contactInvoiceAddress = $this->contactAddressRepository->findOneBy([
'contact' => $contactEntity,
'type' => 1,
]);
$contactDeliveryAddress = $this->contactAddressRepository->findOneBy([
'contact' => $contactEntity,
'type' => 2,
]);
$contactDoc = $this->documentManager
->getRepository(\PSC\Shop\EntityBundle\Document\Contact::class)
->findOneBy(['uid' => $contactEntity->getUid()]);
}
$contactEntity->setCollectingOrders(1);
$contactEntity->setEmail($workSheet->getCell([3, $row->getRowIndex()])->getValue());
$contactEntity->setEnable(true);
$contactEntity->setShops([$selectedShop]);
$contactEntity->setPassword(rand());
$contactEntity->setFirstname((string) $workSheet->getCell([6, $row->getRowIndex()])->getValue());
$contactEntity->setLastname((string) $workSheet->getCell([7, $row->getRowIndex()])->getValue());
$contactEntity->setRolesForm([$role]);
if (!$contactInvoiceAddress) {
$contactInvoiceAddress = new ContactAddress();
}
$contactInvoiceAddress->setSalutation(1);
$contactInvoiceAddress->setFirstname($contactEntity->getFirstname());
$contactInvoiceAddress->setLastname($contactEntity->getLastname());
$contactInvoiceAddress->setContact($contactEntity);
$contactInvoiceAddress->setType(1);
if (!$contactDeliveryAddress) {
$contactDeliveryAddress = new ContactAddress();
}
$contactDeliveryAddress->setSalutation(1);
$contactDeliveryAddress->setFirstname($contactEntity->getFirstname());
$contactDeliveryAddress->setLastname($contactEntity->getLastname());
$contactDeliveryAddress->setContact($contactEntity);
$contactDeliveryAddress->setType(2);
$this->entityManager->persist($contactEntity);
$this->entityManager->persist($contactDeliveryAddress);
$this->entityManager->persist($contactInvoiceAddress);
$this->entityManager->flush();
$contactDoc = new PSCContact();
$contactDoc->setUid($contactEntity->getUid());
$docData = [];
$contactDoc->setLayouterSettings(['collectlayouter' => $docData]);
$this->documentManager->persist($contactDoc);
$this->documentManager->flush();
}
}
return ['form' => $form->createView()];
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Plugin\Custom\Hug\Contact\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
class UploadType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('file', FileType::class, [
'label' => 'Datei',
'required' => true,
]);
$builder->add('import_start_row', TextType::class, [
'label' => 'Import Start Row',
'required' => true,
]);
$builder->add('import_stop_row', TextType::class, [
'label' => 'Import Stop Row',
'required' => true,
]);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Plugin\Custom\Hug\Contact;
use PSC\System\PluginBundle\Plugin\Base;
class Plugin extends Base implements \PSC\System\PluginBundle\Interfaces\Plugin
{
protected $name = 'Contact Importer Hug';
public function getType()
{
return Plugin::Backend;
}
public function getDescription()
{
return 'Contact Importer Hug';
}
public function getVersion()
{
return 1;
}
}

View File

@ -0,0 +1,4 @@
plugin_custom_hug_contact_backend:
resource: "@PluginCustomHugContact/Controller/Backend"
type: annotation
prefix: /backend/plugin/custom/hug/contact

View File

@ -0,0 +1,7 @@
services:
_defaults:
autowire: true
autoconfigure: true
Plugin\Custom\Hug\Contact\:
resource: '../../*/*'

View File

@ -0,0 +1,50 @@
{% extends 'backend_base.html.twig' %}
{% block body %}
<div class="panel">
<div class="header">
<h4>Import</h4>
</div>
<div class="body">
{{ form_start(form, { 'attr': {'class': 'smart-form'}}) }}
{{ form_errors(form) }}
<div class="row">
<div class="col-md-12">
<div class="form-group row">
<div class="col-md-12">
{{ form_label(form.file ) }}
</div>
<div class="col-md-12">
<div class="checkbox">
{{ form_widget(form.file , {attr: {'class': 'form-control'}}) }}
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group row">
<div class="col-md-12">
{{ form_label(form.import_start_row ) }}
</div>
<div class="col-md-12">
{{ form_widget(form.import_start_row , {attr: {'class': 'form-control'}}) }}
</div>
</div>
</div>
<div class="col-md-4">
<div class="form-group row">
<div class="col-md-12">
{{ form_label(form.import_stop_row ) }}
</div>
<div class="col-md-12">
{{ form_widget(form.import_stop_row , {attr: {'class': 'form-control'}}) }}
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-sm btn-primary">Hochladen</button>
{{ form_end(form) }}
</div>
</div>
{% endblock %}

View File

@ -1,14 +1,14 @@
psc_plugin_custom_creativelayouter_backend:
resource: "@PluginSystemPSCCreativeLayouter/Controller/Backend"
resource: "@PluginCustomPSCCreativeLayouter/Controller/Backend"
type: annotation
prefix: /backend/plugin/creativelayouter
psc_shop_motiv_backend:
resource: "@PluginSystemPSCCreativeLayouter/Controller/Backend"
resource: "@PluginCustomPSCCreativeLayouter/Controller/Backend"
type: annotation
prefix: /backend/motiv
psc_shop_motiv_json:
resource: "@PluginSystemPSCCreativeLayouter/Controller/Json"
resource: "@PluginCustomPSCCreativeLayouter/Controller/Json"
type: annotation
prefix: /

View File

@ -1,15 +1,15 @@
<?php
namespace Plugin\System\PSC\TemplateprintLayouter\Dto\Backend\AttachLayouterToOrderPosition;
namespace Plugin\Custom\PSC\TemplateprintLayouter\Dto\Backend\AttachLayouterToOrderPosition;
use Nelmio\ApiDocBundle\Annotation\Model;
use OpenApi\Annotations as OA;
final class Output {
final class Output
{
/**
* @OA\Property(type="boolean")
*/
public bool $success;
}

View File

@ -0,0 +1,71 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is an invoice management frontend built with **Deno v2.0+**, **Vite**, **React 18**, and **TypeScript**. It's part of a larger PrintShopCreator system and builds into `../Resources/public/invoicets`.
The application creates and manages orders with contacts, positions, payments, and shipping information via a REST API backend.
## Build Commands
**Prerequisites**: Deno v2.0.0 or later must be installed.
- **Development server**: `deno task dev`
- **Production build**: `deno task build` (outputs to `../Resources/public/invoicets`)
- **Preview build**: `deno task preview`
- **Serve static files**: `deno task serve` (serves from `dist/`)
## Architecture
### Dependency Injection
Uses `tsyringe-neo` for dependency injection. Services and state are marked with decorators:
- `@singleton()` - single instance across app
- `@autoInjectable()` - auto-inject dependencies
- `@inject(Type)` - inject specific type
Services are resolved using `container.resolve(ServiceClass)`.
### State Management
- **OrderState** (`src/state/order.ts`): Centralized order state using RxJS `BehaviorSubject`
- **Observable Objects** (`src/lib/ob-ob.ts`): Custom reactive proxy pattern that emits RxJS observables on object changes
### API Communication
- All services use Axios with JWT token authentication
- **Token Service** (`src/services/token.ts`): Manages JWT token, auto-refreshes every 2 minutes
- API endpoints are at `/apps/api/*`
- Token is passed via `Authorization: Bearer {token}` header
### Routing
Uses React Router with hash routing (`HashRouter`):
- `/` - New order
- `/:uuid` - Load existing order by UUID
### Key Services
Located in `src/services/`:
- `order.ts` - CRUD operations for orders (get, save, calc)
- `token.ts` - JWT token management
- `contact.ts`, `product.ts`, `shop.ts`, etc. - Domain-specific API services
### Component Structure
Located in `src/modules/`:
- **BaseComponent** - Root component that coordinates all subcomponents
- Organized by domain: `contact/`, `positions/`, `payment/`, `shipping/`, `product/`, etc.
- Modal handling via `@ebay/nice-modal-react`
- Base components in `modules/base/` (Button, Currency, SelectLabel)
### Models
Located in `src/models/`: TypeScript classes for domain entities (Order, Contact, Product, Shop, etc.)
### Entry Point
1. `src/main.tsx` - Initializes app with JWT token from global `jwt_token` variable
2. `src/app/app.tsx` - Creates React root, sets up routing and modal provider
## Development Notes
- Uses Tailwind CSS + Flowbite React components
- Forms built with `@rjsf/core` (JSON Schema forms)
- Async select components with `react-select-async-paginate`
- SWC for fast React transpilation
- Build output uses fixed filenames (no hashes) for easier backend integration

View File

@ -26,9 +26,11 @@
"flowbite-react": "npm:flowbite-react@^0.10.2",
"postcss": "npm:postcss@^8.4.49",
"prop-types": "npm:prop-types@^15.8.1",
"react": "https://esm.sh/react@18.2.0",
"react-dom": "https://esm.sh/react-dom@18.2.0",
"react": "npm:react@^18.2.0",
"react-dom": "npm:react-dom@^18.2.0",
"react-dom/client": "npm:react-dom@^18.2.0/client",
"react-router-dom": "npm:react-router-dom@^7.0.2",
"react-select": "npm:react-select@^5.10.2",
"react-select-async-paginate": "npm:react-select-async-paginate@^0.7.7",
"reflect-metadata": "npm:reflect-metadata@^0.2.2",
"rxjs": "npm:rxjs@^7.8.1",

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,11 @@
import * as Eta from 'eta'
import Token from '../services/token'
import {container} from "tsyringe-neo"
import React from 'react'
import { container } from "tsyringe-neo"
import * as React from 'react'
import ReactDOM from 'react-dom/client'
import NiceModal from "@ebay/nice-modal-react"
import BaseComponent from '../modules/base/BaseComponent'
import {Route, HashRouter as Router, Routes, useSearchParams} from "react-router-dom"
import { Route, HashRouter as Router, Routes, useSearchParams } from "react-router-dom"
export class App {
private searchParams: any
@ -25,17 +25,23 @@ export class App {
}
buildUi() {
const root = ReactDOM.createRoot(document.getElementById('root'))
const rootElement = document.getElementById('root')
if (!rootElement) {
console.error('Root element not found')
return
}
const root = ReactDOM.createRoot(rootElement)
root.render(
<NiceModal.Provider>
<Router>
<NiceModal.Provider>
<Routes>
<Route path="/" element={<BaseComponent />} />
<Route path="/:uuid" element={<BaseComponent />} />
</Routes>
</Router>
</NiceModal.Provider>
</Router>
)
}

View File

@ -2,4 +2,97 @@
@tailwind components;
@tailwind utilities;
@layer base {
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* Better font rendering */
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
@layer components {
/* Card hover effects */
.card-hover {
@apply transition-all duration-200 hover:shadow-lg hover:-translate-y-1;
}
/* Custom scrollbar for webkit browsers */
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
@apply bg-gray-100 dark:bg-gray-800 rounded;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
@apply bg-gray-300 dark:bg-gray-600 rounded hover:bg-gray-400 dark:hover:bg-gray-500;
}
/* Improved select styling */
.select-container {
@apply relative;
}
/* Focus ring improvements */
.focus-ring {
@apply focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900 focus:outline-none;
}
/* Status badges */
.badge {
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
}
.badge-success {
@apply bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100;
}
.badge-warning {
@apply bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100;
}
.badge-error {
@apply bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100;
}
.badge-info {
@apply bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-100;
}
}
@layer utilities {
/* Animation utilities */
.animate-fade-in {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Glass morphism effect */
.glass {
@apply bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm;
}
/* Gradient text */
.gradient-text {
@apply bg-clip-text text-transparent bg-gradient-to-r from-blue-600 to-purple-600;
}
}

View File

@ -3,7 +3,7 @@ import "reflect-metadata";
import * as $ from "jquery";
import { App } from "./app/app";
declare var jwt_token: String;
let jwt_token: String = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE3NTk2NDkyNDMsImV4cCI6MTc1OTY1Mjg0Mywicm9sZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfU0hPUF9PUEVSQVRPUiIsIlJPTEVfVVNFUiIsIlJPTEVfVVNFUiIsIlJPTEVfUFNDX0NvbGxlY3RfQ29udGFjdF9FZGl0IiwiUk9MRV9QU0NfQ29sbGVjdF9Db250YWN0X0FkZCIsIlJPTEVfUFNDX0NvbGxlY3RfQ29udGFjdF9EZWxldGUiLCJST0xFX1BTQ19Db2xsZWN0X0NvbnRhY3RfTG9jayIsIlJPTEVfUFNDX1IyX1NlbmRjbG91ZF9TaG93Il0sInVpZCI6MX0.rTb0nAVWvWkdVhiXeqKaW2xGesOwmLswBD92Ryx1sJ9a1Jlq6EkH0NXyW4quSBV533InhyANeQFITs0mr2d5DDf04MpAd3OENBd3IVQE-_mlMVHNu42-eaxo2xR452hS4yAhKx746xGOnGhw_3gZl07aLgg4qFmb4OYo895XWIM-J-luqy5_3315xINb9Y8P3VTHt-IJ5XPWhpfB7z7QtPCUJuOyJp3nZw6V9bXPiHpcZNG3PELKMEhHyxmjtSOHMfwUhooYKxU21mctQyWTfdiLnKzG4J7nuC3Pgy33_KOoqZZXOO4SyUakFJGZevD25mOzeHCscbTBWMgjjbNK5g";
let app = new App(jwt_token);
app.init();
app.run();

View File

@ -16,6 +16,7 @@ import React from 'react'
import OrderAliasComponent from '../order/OrderAliasComponent'
const BaseComponent = (props) => {
console.log('BaseComponent rendering')
const [shop, setShop] = useState<Shop>(new Shop())
const [order, setOrder] = useState<Order>(new Order())
@ -33,35 +34,55 @@ const BaseComponent = (props) => {
let params = useParams()
useEffect(() => {
console.log('BaseComponent mounted', params)
if(params.uuid) {
loadOrder(params.uuid)
}
}, [])
return (
<React.StrictMode>
<div className="min-h-screen bg-slate-100 text-gray-900 overflow-y-auto dark:text-gray-100 dark:bg-gray-900 antialiased">
<div className='ml-1 mt-1 mr-1'>
<div className="flex gap-1 mt-1 mb-1">
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-gray-900 dark:to-gray-800 text-gray-900 dark:text-gray-100 antialiased">
<div className='container mx-auto px-4 py-6 max-w-7xl'>
{/* Header Section */}
<div className="flex items-center justify-between gap-4 mb-6">
<div className="flex-1">
<OrderAliasComponent order={order} />
</div>
<div className="">
<div>
<ButtonComponent loadOrder={loadOrder}/>
</div>
</div>
{/* Top Bar Section */}
<div className="mb-6">
<TopBarComponent shop={shop} order={order} change={setShop} />
<div className="mt-1 mb-1">{ shop.id != 0 && <ContactComponent shop={shop} order={order} /> }</div>
<div>{ shop.id != 0 && <PositionsComponent shop={shop} order={order} /> }</div>
<div className="mt-1 mb-1 flex gap-1">
<div className="flex-1">{ shop.id != 0 && <PaymentComponent shop={shop} order={order} /> }</div>
<div className="flex-1">{ shop.id != 0 && <ShippingComponent shop={shop} order={order} /> }</div>
</div>
{ shop.id != 0 && <InfoFieldComponent shop={shop} order={order} /> }
{/* Main Content */}
{ shop.id != 0 && (
<>
{/* Contact Section */}
<div className="mb-6">
<ContactComponent shop={shop} order={order} />
</div>
{/* Positions Section */}
<div className="mb-6">
<PositionsComponent shop={shop} order={order} />
</div>
{/* Payment & Shipping Row */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<PaymentComponent shop={shop} order={order} />
<ShippingComponent shop={shop} order={order} />
</div>
{/* Info Section */}
<InfoFieldComponent shop={shop} order={order} />
</>
)}
</div>
</div>
</React.StrictMode>
)
}

View File

@ -1,17 +1,22 @@
import styled from "styled-components"
import { components } from "react-select"
import React from 'react'
import * as React from 'react'
export const SelectLabel = (props: any) => {
export const SelectLabel = ({ children, ...props }: any) => {
const hasValue = props.hasValue || (props.getValue && props.getValue().length > 0 && props.getValue()[0]?.uuid !== "");
return (
<>
<components.Control {...props} />
<Label $isFloating={props.getValue().length == 0 || props.getValue()[0].uuid == ""}>{props.selectProps.name}</Label>
</>
<Wrapper>
<components.Control {...props} children={children} />
<Label $isFloating={!hasValue}>{props.selectProps.name}</Label>
</Wrapper>
);
};
const Wrapper = styled.div`
position: relative;
`;
const Label = styled.label<{ $isFloating?: boolean }>`
left: 10px;
pointer-events: none;

View File

@ -51,16 +51,30 @@ class ButtonComponent extends Component<{loadOrder},{disabled: boolean}> {
render() {
return (
<div className="flex gap-3">
<Button size="xs" color="info" disabled={this.state.disabled} onClick={(e:any) => this.handleSave(e)}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon" fill="none" class="mr-2 h-5 w-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 20.25h12A2.25 2.25 0 0 0 20.25 18V7.5L16.5 3.75H6A2.25 2.25 0 0 0 3.75 6v12A2.25 2.25 0 0 0 6 20.25zm9.75-16.5v5h-9.5v-5zM13 5.5V7m-6.75 4.25h11.5v6.5H6.25Z"></path>
</svg> Speichern
<div className="flex gap-3 flex-wrap">
<Button
size="md"
color="blue"
disabled={this.state.disabled}
onClick={(e:any) => this.handleSave(e)}
className="shadow-md hover:shadow-lg transition-shadow"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" fill="none" className="mr-2 h-5 w-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 20.25h12A2.25 2.25 0 0 0 20.25 18V7.5L16.5 3.75H6A2.25 2.25 0 0 0 3.75 6v12A2.25 2.25 0 0 0 6 20.25zm9.75-16.5v5h-9.5v-5zM13 5.5V7m-6.75 4.25h11.5v6.5H6.25Z"></path>
</svg>
Speichern
</Button>
<Button size="xs" color="success" disabled={!this.orderState.getCurrentOrder().value.saved} onClick={(e:any) => this.handlePrint(e)}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon" class="mr-2 h-5 w-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0 1 10.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0 .229 2.523a1.125 1.125 0 0 1-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0 0 21 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 0 0-1.913-.247M6.34 18H5.25A2.25 2.25 0 0 1 3 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 0 1 1.913-.247m10.5 0a48.536 48.536 0 0 0-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5Zm-3 0h.008v.008H15V10.5Z"></path>
</svg> Drucken
<Button
size="md"
color="success"
disabled={!this.orderState.getCurrentOrder().value.saved}
onClick={(e:any) => this.handlePrint(e)}
className="shadow-md hover:shadow-lg transition-shadow"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="mr-2 h-5 w-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0 1 10.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0 .229 2.523a1.125 1.125 0 0 1-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0 0 21 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 0 0-1.913-.247M6.34 18H5.25A2.25 2.25 0 0 1 3 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 0 1 1.913-.247m10.5 0a48.536 48.536 0 0 0-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5Zm-3 0h.008v.008H15V10.5Z"></path>
</svg>
Drucken
</Button>
</div>
)

View File

@ -10,7 +10,6 @@ import {container} from "tsyringe-neo"
import AccountSelectComponent from '../account/AccountSelectComponent'
import Order from '../../model/order'
import { Shop } from '../../model/shop'
import React from 'react'
const ContactComponent = ({order, shop}) => {
@ -34,54 +33,73 @@ const ContactComponent = ({order, shop}) => {
}
return (
<>
<div className="flex">
<div className="flex-1">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 animate-fade-in">
<h2 className="text-xl font-semibold mb-4 text-gray-800 dark:text-gray-100 flex items-center gap-2">
<svg className="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
Kontakt & Konto
</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
<div>
<ContactSelect
order={order}
shop={shop}
onChange={setContact}
/>
</div>
<div className="flex-1">
<div>
<AccountSelectComponent
shop={shop} order={order}
/>
</div>
</div>
<div className="flex gap-1">
<div className="flex-1">
{ order.contact.uuid != "" && <AddressSelect
{ order.contact.uuid != "" && (
<>
<h3 className="text-lg font-medium mb-3 text-gray-700 dark:text-gray-200 flex items-center gap-2">
<svg className="w-5 h-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Adressen
</h3>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div>
<AddressSelect
address={order.invoiceAddress}
contact={order.contact}
changeAddress={changeAddress}
name='Rechnungsadresse'
type={1}
shop={shop}
/> }
/>
</div>
<div className="flex-1">
{ order.contact.uuid != "" && <AddressSelect
<div>
<AddressSelect
address={order.deliveryAddress}
contact={order.contact}
changeAddress={changeAddress}
name='Lieferadresse'
type={2}
shop={shop}
/> }
/>
</div>
<div className="flex-1">
{ order.contact.uuid != "" && <AddressSelect
<div>
<AddressSelect
address={order.senderAddress}
changeAddress={changeAddress}
contact={order.contact}
name='Absenderadresse'
type={3}
shop={shop}
/> }
/>
</div>
</div>
</>
)}
</div>
)
}

View File

@ -44,7 +44,13 @@ const PaymentComponent = ({ shop, order }) => {
}
return (
<div className={"p-2"}>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 animate-fade-in">
<h2 className="text-xl font-semibold mb-4 text-gray-800 dark:text-gray-100 flex items-center gap-2">
<svg className="w-6 h-6 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
</svg>
Zahlart
</h2>
<AsyncPaginate
defaultOptions
key={JSON.stringify(shopUuid)}
@ -57,7 +63,7 @@ const PaymentComponent = ({ shop, order }) => {
getOptionLabel={(option) => option.title}
onChange={onChange}
isSearchable={false}
className={`${payment == 0 ? "border border-red-500" : ""}`}
className={`${payment == 0 ? "border-2 border-red-500 rounded" : ""}`}
/>
</div>
)

View File

@ -11,7 +11,6 @@ import Order from "../../model/order"
import { Pos } from '../../model/pos'
import Button from '../base/Button'
import { Modal } from "flowbite-react";
import React from 'react'
const AddPositionComponent = (props) => {

View File

@ -9,36 +9,42 @@ const ItemsComponent = ({positions, delPos, shop, changePos}) => {
return (
<div>
<div className={'flex'}>
<div className={'flex-1'}>
<h5>Pos</h5>
</div>
<div className={'flex-1'}>
<h5>Title</h5>
</div>
<div className={'flex-1'}>
<h5>Anzahl</h5>
</div>
<div className={'flex-1 text-end'}>
<h5>Netto</h5>
</div>
<div className={'flex-1 text-end'}>
<h5>Tax</h5>
</div>
<div className={'flex-1 text-end'}>
<h5>Gross</h5>
</div>
<div className={'flex-1 text-end'}>
<h5>Status</h5>
</div>
<div className={'flex-1'}>
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="border-b-2 border-gray-200 dark:border-gray-700">
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700 dark:text-gray-300">Pos</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700 dark:text-gray-300">Titel</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700 dark:text-gray-300">Anzahl</th>
<th className="text-right py-3 px-4 text-sm font-semibold text-gray-700 dark:text-gray-300">Netto</th>
<th className="text-right py-3 px-4 text-sm font-semibold text-gray-700 dark:text-gray-300">MwSt</th>
<th className="text-right py-3 px-4 text-sm font-semibold text-gray-700 dark:text-gray-300">Brutto</th>
<th className="text-right py-3 px-4 text-sm font-semibold text-gray-700 dark:text-gray-300">Status</th>
<th className="py-3 px-4 text-sm font-semibold text-gray-700 dark:text-gray-300"></th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{positions.map((object, i) => (
<PosComponent
pos={object}
shop={shop}
delPos={delPos}
changePos={changePos}
key={i}
index={i}
/>
))}
</tbody>
</table>
{positions.length === 0 && (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
<svg className="w-16 h-16 mx-auto mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<p className="text-lg">Keine Positionen vorhanden</p>
</div>
</div>
<hr/>
{positions.map((object, i) => <PosComponent pos={object} shop={shop} delPos={delPos} changePos={changePos} key={i} index={i} />)}
)}
</div>
)
}

View File

@ -14,24 +14,27 @@ const PosComponent = ({index, pos, delPos, changePos, shop}) => {
}
return (
<>
<div className={'flex'}>
<div className={'flex-1'}>{index + 1}</div>
<div className={'flex-1'}>{pos.product.title}</div>
<div className={'flex-1'}>{pos.count}</div>
<div className={'flex-1 text-end'}><Currency price={ pos.price.allNet} /></div>
<div className={'flex-1 text-end'}><Currency price={ pos.price.allVat} /></div>
<div className={'flex-1 text-end'}><Currency price={ pos.price.allGross} /></div>
<div className={'flex-1 text-end'}></div>
<div className={'flex-1 text-end'}>
<div className="flex gap-1">
<tr className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<td className="py-3 px-4 text-sm text-gray-700 dark:text-gray-300">{index + 1}</td>
<td className="py-3 px-4 text-sm text-gray-700 dark:text-gray-300 font-medium">{pos.product.title}</td>
<td className="py-3 px-4 text-sm text-gray-700 dark:text-gray-300">{pos.count}</td>
<td className="py-3 px-4 text-sm text-right text-gray-700 dark:text-gray-300">
<Currency price={pos.price.allNet} />
</td>
<td className="py-3 px-4 text-sm text-right text-gray-700 dark:text-gray-300">
<Currency price={pos.price.allVat} />
</td>
<td className="py-3 px-4 text-sm text-right text-gray-700 dark:text-gray-300 font-semibold">
<Currency price={pos.price.allGross} />
</td>
<td className="py-3 px-4 text-sm text-right text-gray-700 dark:text-gray-300"></td>
<td className="py-3 px-4 text-sm text-right">
<div className="flex gap-2 justify-end">
<EditPositionComponent shop={shop} position={pos} changePos={changePos} />
<Button type={5} variant={'failure'} onClick={() => deletePos(pos.uuid)} />
</div>
</div>
</div>
<hr/>
</>
</td>
</tr>
)
}

View File

@ -43,10 +43,18 @@ const PositionsComponent = ({order, shop, updateOrder}) => {
}, [positions])
return (
<div className={"p-2"}>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 animate-fade-in">
<h2 className="text-xl font-semibold mb-4 text-gray-800 dark:text-gray-100 flex items-center gap-2">
<svg className="w-6 h-6 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
Positionen
</h2>
<ItemsComponent positions={positions} shop={shop} delPos={delPos} changePos={changePos}/>
<div className="mt-4">
<AddPositionComponent shop={shop} addPos={addPos}/>
</div>
</div>
)
}

View File

@ -8,7 +8,6 @@ import OrderState from "../../state/order"
import Order from "../../model/order"
import {useEffect, useState} from "react"
import { SelectLabel } from '../base/SelectLabel'
import React from 'react'
const ShippingComponent = ({shop, order}) => {
@ -43,7 +42,13 @@ const ShippingComponent = ({shop, order}) => {
}
return (
<div className={"p-2"}>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 animate-fade-in">
<h2 className="text-xl font-semibold mb-4 text-gray-800 dark:text-gray-100 flex items-center gap-2">
<svg className="w-6 h-6 text-orange-600 dark:text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
</svg>
Versandart
</h2>
<AsyncPaginate
defaultOptions
key={JSON.stringify(shopUuid)}
@ -56,10 +61,10 @@ const ShippingComponent = ({shop, order}) => {
getOptionLabel={(option) => option.title}
onChange={onChange}
isSearchable={false}
className={`${shipping == 0 ? "border border-red-500" : ""}`}
className={`${shipping == 0 ? "border-2 border-red-500 rounded" : ""}`}
/>
</div>
)
)
}
ShippingComponent.propTypes = {

View File

@ -10,20 +10,22 @@ import React from 'react'
const TopBarComponent = ({shop, order, change}) => {
return (
<div className="flex gap-3">
<div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 animate-fade-in">
<div className="flex flex-wrap items-center gap-4">
<div className="w-auto">
<DraftComponent order={order} />
</div>
<div className="flex-auto">
<div className="flex-1 min-w-[200px]">
<TypeSelectComponent order={order} />
</div>
<div className="flex-auto">
<div className="flex-1 min-w-[200px]">
<ShopSelectComponent shop={shop} change={change} />
</div>
<div>
<div className="w-auto">
<CalcComponent />
</div>
</div>
</div>
)
}

View File

@ -3,7 +3,7 @@
"target": "ES2020",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"useDefineForClassFields": true,
"useDefineForClassFields": false,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
@ -11,15 +11,15 @@
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"isolatedModules": false,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"strict": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]

View File

@ -4,16 +4,47 @@ import react from '@vitejs/plugin-react-swc'
// https://vite.dev/config/
export default defineConfig({
plugins: [deno(), react()],
plugins: [
deno(),
react({
tsDecorators: true
})
],
server: {
proxy: {
'/apps': {
target: 'http://type-dev-tp.local',
changeOrigin: true,
secure: false
}
}
},
resolve: {
dedupe: ['react', 'react-dom']
},
build: {
rollupOptions: {
output: {
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].[ext]`
assetFileNames: `assets/[name].[ext]`,
format: 'iife',
inlineDynamicImports: true
}
},
outDir: '../Resources/public/invoicets',
emptyOutDir: true, // also necessary
emptyOutDir: true,
target: 'es2015'
},
esbuild: {
tsconfigRaw: {
compilerOptions: {
experimentalDecorators: true,
emitDecoratorMetadata: true
}
}
},
optimizeDeps: {
include: ['react', 'react-dom', 'react-dom/client']
}
})

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<script type="module" crossorigin src="/assets/index.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index.css">
</head>
<body>
<div id="root"></div>

View File

@ -81,7 +81,7 @@ class GetPrice extends AbstractController
description: 'get price',
content: new JsonContent(ref: new Model(type: PriceOutput::class)),
)]
#[RequestBody(ref: new Model(type: PriceInput::class))]
#[RequestBody(content: new JsonContent(ref: new Model(type: PriceInput::class)))]
#[Tag(name: 'Plugin/System/psc/Xmlcalc/Price')]
#[Route(path: '/price', methods: ['POST'])]
#[ParamConverter(

View File

@ -37,7 +37,7 @@ class Config extends AbstractController
description: 'get config for product',
content: new JsonContent(ref: new Model(type: PluginProduct::class)),
)]
#[RequestBody(ref: new Model(type: PriceInput::class))]
#[RequestBody(content: new JsonContent(ref: new Model(type: PriceInput::class)))]
#[Tag(name: 'Plugin/System/psc/Xmlcalc/Product')]
#[Route(path: '/product/config', methods: ['POST'])]
#[ParamConverter(

View File

@ -40,7 +40,7 @@ class Design extends AbstractController
description: 'get config for product',
content: new JsonContent(ref: new Model(type: PluginProduct::class)),
)]
#[RequestBody(ref: new Model(type: DesignInput::class))]
#[RequestBody(content: new JsonContent(ref: new Model(type: DesignInput::class)))]
#[Tag(name: 'Plugin/System/psc/Xmlcalc/Product')]
#[Route(path: '/product/design', methods: ['POST'])]
#[ParamConverter(

View File

@ -40,7 +40,7 @@ class Json extends AbstractController
description: 'get config for product',
content: new JsonContent(ref: new Model(type: PluginProduct::class)),
)]
#[RequestBody(ref: new Model(type: JsonInput::class))]
#[RequestBody(content: new JsonContent(ref: new Model(type: JsonInput::class)))]
#[Tag(name: 'Plugin/System/psc/Xmlcalc/Product')]
#[Route(path: '/product/json', methods: ['POST'])]
#[ParamConverter(

View File

@ -81,7 +81,7 @@ class Preview extends AbstractController
description: 'generate preview',
content: new JsonContent(ref: new Model(type: PriceOutput::class)),
)]
#[RequestBody(ref: new Model(type: PriceInput::class))]
#[RequestBody(content: new JsonContent(ref: new Model(type: PriceInput::class)))]
#[Tag(name: 'Plugin/System/psc/Xmlcalc/Price')]
#[Route(path: '/product/preview', methods: ['POST'])]
#[ParamConverter(

View File

@ -78,7 +78,7 @@ class PreviewDesigner extends AbstractController
content: new JsonContent(ref: new Model(type: PDOutput::class)),
)]
#[Tag(name: 'Plugin/System/psc/Xmlcalc/Price')]
#[RequestBody(ref: new Model(type: PDInput::class))]
#[RequestBody(content: new JsonContent(ref: new Model(type: PDInput::class)))]
#[ParamConverter('data', class: '\Plugin\System\PSC\XmlCalc\Dto\Input\PDInput', converter: 'psc_rest.request_body')]
public function preview(PDInput $data)
{

View File

@ -33,7 +33,7 @@ class Update extends AbstractController
content: new JsonContent(ref: new Model(type: Product::class)),
)]
#[Security(name: 'Bearer')]
#[RequestBody(ref: new Model(type: Product::class))]
#[RequestBody(content: new JsonContent(ref: new Model(type: Product::class)))]
#[Tag(name: 'Plugin/System/psc/Xmlcalc/Product')]
#[Route(path: '/product/{uuid}', methods: ['PUT'])]
#[ParamConverter(

View File

@ -40,7 +40,7 @@ class XML extends AbstractController
description: 'get config for product',
content: new JsonContent(ref: new Model(type: PluginProduct::class)),
)]
#[RequestBody(ref: new Model(type: XMLInput::class))]
#[RequestBody(content: new JsonContent(ref: new Model(type: XMLInput::class)))]
#[Tag(name: 'Plugin/System/psc/Xmlcalc/Product')]
#[Route(path: '/product/xml', methods: ['POST'])]
#[ParamConverter(