feat: add api-platform and symfony/uid dependencies; introduce billing module service configuration

- Added api-platform/symfony version 4.1 with necessary configuration files.
- Included symfony/uid version 7.1 with its recipe details.
- Created billing_module.yaml to define the BillingModulePlugin service with autowiring and autoconfiguration.
- Added SoftwareBuddy agent for web development support in PHP (Symfony) and JavaScript (Vue.js).
This commit is contained in:
olli 2025-12-05 15:07:37 +01:00
parent 6b5e82cd2e
commit 5ffd7bd0d1
7 changed files with 1762 additions and 271 deletions

5
.github/agents/SoftwareBuddy.agent.md vendored Normal file
View File

@ -0,0 +1,5 @@
---
description: 'Du bist mein Software-Sparringspartner, der mich in der Webentwicklung unterstützt.'
tools: ['edit', 'runNotebooks', 'search', 'new', 'runCommands', 'runTasks', 'usages', 'vscodeAPI', 'problems', 'changes', 'testFailure', 'openSimpleBrowser', 'fetch', 'githubRepo', 'extensions', 'todos', 'runSubagent', 'runTests']
---
Bei Fragen zur Webentwicklung, insbesondere im Bereich PHP (Symfony) und JavaScript (Vue.js), agierst du als mein Software-Sparringspartner. Du hilfst mir, komplexe Probleme zu lösen, indem du fundierte Ratschläge gibst, Best Practices empfiehlst und mich durch schwierige Debugging-Situationen führst. Du bist stets darauf bedacht, sauberen, wartbaren und effizienten Code zu fördern. Du kennst dich gut mit modernen Entwicklungswerkzeugen und -prozessen aus, einschließlich Versionskontrolle (Git), Paketverwaltung (Composer, npm), Testing-Frameworks und CI/CD-Pipelines. Du unterstützt mich dabei, meine Fähigkeiten zu verbessern, indem du konstruktives Feedback zu meinem Code gibst und mich ermutigst, neue Technologien und Methoden auszuprobieren. Du übernimmst auch die Rolle des Entwicklers und schreibst sauberen, gut dokumentierten Code, wenn ich dich darum bitte.

View File

@ -8,7 +8,12 @@
<!-- Rechnungsnummer -->
<div class="col-12 md:col-6">
<label for="invoiceNumber">Rechnungsnummer *</label>
<InputText id="invoiceNumber" v-model="form.invoiceNumber" required />
<InputText
id="invoiceNumber"
v-model="form.invoiceNumber"
:class="{ 'p-invalid': submitted && !form.invoiceNumber }"
/>
<small v-if="submitted && !form.invoiceNumber" class="p-error">Rechnungsnummer ist erforderlich</small>
</div>
<!-- Status -->
@ -30,11 +35,13 @@
id="contact"
v-model="form.contactId"
:options="contacts"
option-label="name"
option-label="companyName"
option-value="id"
filter
placeholder="Kunde auswählen"
:class="{ 'p-invalid': submitted && !form.contactId }"
/>
<small v-if="submitted && !form.contactId" class="p-error">Bitte wählen Sie einen Kunden aus</small>
</div>
<!-- Rechnungsdatum -->
@ -92,14 +99,15 @@
<!-- Actions -->
<div class="flex gap-2 mt-4">
<Button label="Speichern" icon="pi pi-check" @click="save" />
<Button label="Abbrechen" icon="pi pi-times" severity="secondary" text @click="cancel" />
<Button label="Speichern" icon="pi pi-check" @click="save" :loading="saving" />
<Button label="Abbrechen" icon="pi pi-times" severity="secondary" text @click="cancel" :disabled="saving" />
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useToast } from 'primevue/usetoast'
import InputText from 'primevue/inputtext'
import Dropdown from 'primevue/dropdown'
import Calendar from 'primevue/calendar'
@ -116,6 +124,10 @@ const props = defineProps({
const emit = defineEmits(['save', 'cancel'])
const toast = useToast()
const submitted = ref(false)
const saving = ref(false)
const form = ref({
invoiceNumber: '',
status: 'draft',
@ -141,13 +153,48 @@ const taxRates = ['0.00', '7.00', '19.00']
onMounted(async () => {
// Load contacts
const response = await fetch('/api/contacts')
const data = await response.json()
contacts.value = data['hydra:member'] || []
try {
const response = await fetch('/api/contacts?itemsPerPage=1000', {
credentials: 'include',
headers: {
'Accept': 'application/ld+json'
}
})
if (!response.ok) {
throw new Error('Fehler beim Laden der Kontakte')
}
const data = await response.json()
// API Platform kann 'hydra:member' oder 'member' verwenden
contacts.value = data['hydra:member'] || data.member || []
console.log('Loaded contacts:', contacts.value.length)
} catch (error) {
console.error('Error loading contacts:', error)
toast.add({
severity: 'error',
summary: 'Fehler',
detail: 'Kontakte konnten nicht geladen werden',
life: 3000
})
}
// Load existing invoice data
if (props.invoice) {
form.value = { ...props.invoice }
// Extract contact ID from IRI if needed
if (props.invoice.contact) {
if (typeof props.invoice.contact === 'string') {
// Extract ID from IRI like "/api/contacts/123"
const matches = props.invoice.contact.match(/\/api\/contacts\/(\d+)/)
form.value.contactId = matches ? parseInt(matches[1]) : null
} else if (props.invoice.contact.id) {
form.value.contactId = props.invoice.contact.id
}
}
if (props.invoice.invoiceDate) {
form.value.invoiceDate = new Date(props.invoice.invoiceDate)
}
@ -171,26 +218,87 @@ const removeItem = (index) => {
}
const save = async () => {
const method = props.invoice ? 'PUT' : 'POST'
const url = props.invoice ? `/api/invoices/${props.invoice.id}` : '/api/invoices'
submitted.value = true
const payload = {
invoiceNumber: form.value.invoiceNumber,
contact: `/api/contacts/${form.value.contactId}`,
status: form.value.status,
invoiceDate: form.value.invoiceDate.toISOString().split('T')[0],
dueDate: form.value.dueDate.toISOString().split('T')[0],
notes: form.value.notes,
items: form.value.items
// Validation
if (!form.value.invoiceNumber || !form.value.contactId) {
toast.add({
severity: 'warn',
summary: 'Validierung fehlgeschlagen',
detail: 'Bitte füllen Sie alle Pflichtfelder aus',
life: 3000
})
return
}
await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
if (form.value.items.length === 0) {
toast.add({
severity: 'warn',
summary: 'Validierung fehlgeschlagen',
detail: 'Bitte fügen Sie mindestens eine Position hinzu',
life: 3000
})
return
}
emit('save', form.value)
saving.value = true
try {
const method = props.invoice ? 'PUT' : 'POST'
const url = props.invoice ? `/api/invoices/${props.invoice.id}` : '/api/invoices'
// Konvertiere alle numerischen Werte in Items zu Strings (für DECIMAL Felder)
const items = form.value.items.map(item => ({
description: item.description,
quantity: String(item.quantity),
unitPrice: String(item.unitPrice),
taxRate: String(item.taxRate)
}))
const payload = {
invoiceNumber: form.value.invoiceNumber,
contact: `/api/contacts/${form.value.contactId}`,
status: form.value.status,
invoiceDate: form.value.invoiceDate.toISOString().split('T')[0],
dueDate: form.value.dueDate.toISOString().split('T')[0],
notes: form.value.notes,
items: items
}
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/ld+json',
'Accept': 'application/ld+json'
},
credentials: 'include',
body: JSON.stringify(payload)
})
if (!response.ok) {
const error = await response.json()
throw new Error(error['hydra:description'] || error.message || 'Fehler beim Speichern')
}
toast.add({
severity: 'success',
summary: 'Erfolg',
detail: `Rechnung wurde ${props.invoice ? 'aktualisiert' : 'erstellt'}`,
life: 3000
})
emit('save', form.value)
} catch (error) {
console.error('Error saving invoice:', error)
toast.add({
severity: 'error',
summary: 'Fehler',
detail: error.message || 'Rechnung konnte nicht gespeichert werden',
life: 5000
})
} finally {
saving.value = false
}
}
const cancel = () => {

View File

@ -4,10 +4,12 @@
<CrudDataTable
title="Rechnungen"
:api-endpoint="`/api/invoices`"
entity-name="Rechnung"
entity-name-article="eine"
data-source="/api/invoices"
storage-key="invoiceTableColumns"
:columns="columns"
:show-create-button="canCreate"
create-button-label="Neue Rechnung"
@create="openCreateDialog"
@edit="openEditDialog"
@delete="deleteInvoice"

View File

@ -15,7 +15,7 @@
"doctrine/orm": "^3.5",
"knpuniversity/oauth2-client-bundle": "*",
"league/oauth2-client": "*",
"mycrm/billing-module": "@dev",
"mycrm/billing-module": "^1.0",
"mycrm/test-module": "*",
"nelmio/cors-bundle": "^2.6",
"phpdocumentor/reflection-docblock": "^5.6",
@ -85,8 +85,7 @@
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd",
"importmap:install": "symfony-cmd"
"assets:install %PUBLIC_DIR%": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
@ -121,8 +120,8 @@
"url": "https://git.osdata-home.de/mycrm/mycrm-test-module"
},
"mycrm-billing-module": {
"type": "path",
"url": "../mycrm-billing-module"
"type": "vcs",
"url": "https://git.osdata-home.de/mycrm/mycrm-billing-module"
}
}
}

1830
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
services:
MyCRM\BillingModule\BillingModulePlugin:
autowire: true
autoconfigure: true
tags:
- { name: 'app.module_plugin' }

View File

@ -13,6 +13,20 @@
"src/ApiResource/.gitignore"
]
},
"api-platform/symfony": {
"version": "4.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "4.0",
"ref": "e9952e9f393c2d048f10a78f272cd35e807d972b"
},
"files": [
"config/packages/api_platform.yaml",
"config/routes/api_platform.yaml",
"src/ApiResource/.gitignore"
]
},
"doctrine/deprecations": {
"version": "1.1",
"recipe": {
@ -300,6 +314,15 @@
"templates/base.html.twig"
]
},
"symfony/uid": {
"version": "7.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.0",
"ref": "0df5844274d871b37fc3816c57a768ffc60a43a5"
}
},
"symfony/ux-turbo": {
"version": "2.31",
"recipe": {