Various Changes
- Added support for Streamdeck Pedal - Changed UI to Packed Theme - Added preview for knobs (Loupedeck Live) - Added Start to Tray - Added Udev Rules for Streamdeck Pedal
This commit is contained in:
@@ -20,6 +20,10 @@ Sidenote: Please excuse my terrible code, i usually only code in C / Cpp
|
|||||||
- After downloading, make it executable and run it:
|
- After downloading, make it executable and run it:
|
||||||
- `chmod +x Packed*.AppImage`
|
- `chmod +x Packed*.AppImage`
|
||||||
- `./Packed*.AppImage`
|
- `./Packed*.AppImage`
|
||||||
|
- Install the udev rules:
|
||||||
|
<sudo cp udev/50-loupedeck-user.rules /etc/udev/rules.d/
|
||||||
|
sudo cp udev/50-streamdeck-user.rules /etc/udev/rules.d/
|
||||||
|
sudo udevadm control --reload-rules>
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
|
|||||||
56
main.js
56
main.js
@@ -10,6 +10,8 @@ import { PedalPageManager } from './src/streamdeck/pages.js';
|
|||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
const START_IN_TRAY_ARG = '--start-in-tray';
|
||||||
|
const startInTray = process.argv.includes(START_IN_TRAY_ARG);
|
||||||
|
|
||||||
function getTrayIcon() {
|
function getTrayIcon() {
|
||||||
const iconPath = path.join(__dirname, 'assets', 'icons', 'Icon.png');
|
const iconPath = path.join(__dirname, 'assets', 'icons', 'Icon.png');
|
||||||
@@ -25,13 +27,23 @@ let deviceStatus = { connected: false };
|
|||||||
let pedalDevice;
|
let pedalDevice;
|
||||||
let pedalPageManager;
|
let pedalPageManager;
|
||||||
let pedalStatus = { connected: false };
|
let pedalStatus = { connected: false };
|
||||||
|
const startupWarnings = new Map();
|
||||||
|
|
||||||
function createWindow() {
|
function pushRuntimeWarning(data) {
|
||||||
|
if (!data || !data.code) return;
|
||||||
|
startupWarnings.set(data.code, data);
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed() && mainWindow.webContents) {
|
||||||
|
mainWindow.webContents.send('runtime-warning', data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWindow(startHidden = false) {
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1580,
|
width: 1580,
|
||||||
height: 800,
|
height: 800,
|
||||||
minWidth: 1380,
|
minWidth: 1380,
|
||||||
minHeight: 600,
|
minHeight: 600,
|
||||||
|
show: !startHidden,
|
||||||
frame: false,
|
frame: false,
|
||||||
icon: path.join(__dirname, 'assets', 'icons', 'Icon.png'),
|
icon: path.join(__dirname, 'assets', 'icons', 'Icon.png'),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
@@ -47,6 +59,10 @@ function createWindow() {
|
|||||||
|
|
||||||
mainWindow.webContents.on('did-finish-load', () => {
|
mainWindow.webContents.on('did-finish-load', () => {
|
||||||
mainWindow.webContents.send('device-status', deviceStatus);
|
mainWindow.webContents.send('device-status', deviceStatus);
|
||||||
|
mainWindow.webContents.send('pedal-status', pedalStatus);
|
||||||
|
startupWarnings.forEach((warning) => {
|
||||||
|
mainWindow.webContents.send('runtime-warning', warning);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.on('close', (event) => {
|
mainWindow.on('close', (event) => {
|
||||||
@@ -57,6 +73,19 @@ function createWindow() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyAutostartToTraySetting(enabled) {
|
||||||
|
const openAtLogin = !!enabled;
|
||||||
|
try {
|
||||||
|
app.setLoginItemSettings({
|
||||||
|
openAtLogin,
|
||||||
|
openAsHidden: openAtLogin,
|
||||||
|
args: openAtLogin ? [START_IN_TRAY_ARG] : []
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not set login item settings:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function createTray() {
|
function createTray() {
|
||||||
tray = new Tray(getTrayIcon());
|
tray = new Tray(getTrayIcon());
|
||||||
const contextMenu = Menu.buildFromTemplate([
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
@@ -69,8 +98,11 @@ function createTray() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function initializeLoupedeck() {
|
async function initializeLoupedeck() {
|
||||||
configManager = new ConfigManager();
|
if (!configManager) {
|
||||||
await configManager.load();
|
configManager = new ConfigManager();
|
||||||
|
await configManager.load();
|
||||||
|
}
|
||||||
|
applyAutostartToTraySetting(configManager.getSetting('autostartToTray'));
|
||||||
|
|
||||||
pageManager = new PageManager(configManager);
|
pageManager = new PageManager(configManager);
|
||||||
|
|
||||||
@@ -106,6 +138,10 @@ async function initializeLoupedeck() {
|
|||||||
mainWindow.webContents.send('button-toggle', data);
|
mainWindow.webContents.send('button-toggle', data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
loupedeckDevice.on('runtime-warning', (data) => {
|
||||||
|
pushRuntimeWarning(data);
|
||||||
|
});
|
||||||
|
|
||||||
await loupedeckDevice.connect();
|
await loupedeckDevice.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +163,10 @@ async function initializePedal() {
|
|||||||
mainWindow.webContents.send('pedal-button-press', data);
|
mainWindow.webContents.send('pedal-button-press', data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pedalDevice.on('runtime-warning', (data) => {
|
||||||
|
pushRuntimeWarning(data);
|
||||||
|
});
|
||||||
|
|
||||||
await pedalDevice.connect();
|
await pedalDevice.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,6 +369,9 @@ ipcMain.handle('reconnect-device', async () => {
|
|||||||
ipcMain.handle('set-setting', async (event, { key, value }) => {
|
ipcMain.handle('set-setting', async (event, { key, value }) => {
|
||||||
const previousAccent = key === 'accentColor' ? configManager.getSetting('accentColor') : null;
|
const previousAccent = key === 'accentColor' ? configManager.getSetting('accentColor') : null;
|
||||||
configManager.setSetting(key, value);
|
configManager.setSetting(key, value);
|
||||||
|
if (key === 'autostartToTray') {
|
||||||
|
applyAutostartToTraySetting(value);
|
||||||
|
}
|
||||||
if (key === 'accentColor') {
|
if (key === 'accentColor') {
|
||||||
const config = configManager.getConfig();
|
const config = configManager.getConfig();
|
||||||
if (Array.isArray(config.pages)) {
|
if (Array.isArray(config.pages)) {
|
||||||
@@ -413,7 +456,10 @@ ipcMain.handle('window-close', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
createWindow();
|
configManager = new ConfigManager();
|
||||||
|
await configManager.load();
|
||||||
|
const startHidden = startInTray || !!configManager.getSetting('autostartToTray');
|
||||||
|
createWindow(startHidden);
|
||||||
|
|
||||||
// Tray nur bauen, wenn das Icon da ist
|
// Tray nur bauen, wenn das Icon da ist
|
||||||
try {
|
try {
|
||||||
@@ -427,7 +473,7 @@ app.whenReady().then(async () => {
|
|||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
createWindow();
|
createWindow(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
1098
node_modules/.package-lock.json
generated
vendored
1098
node_modules/.package-lock.json
generated
vendored
File diff suppressed because it is too large
Load Diff
BIN
node_modules/canvas/build/Release/canvas-postbuild.node
generated
vendored
BIN
node_modules/canvas/build/Release/canvas-postbuild.node
generated
vendored
Binary file not shown.
BIN
node_modules/canvas/build/Release/canvas.node
generated
vendored
BIN
node_modules/canvas/build/Release/canvas.node
generated
vendored
Binary file not shown.
BIN
node_modules/canvas/build/Release/obj.target/canvas-postbuild.node
generated
vendored
BIN
node_modules/canvas/build/Release/obj.target/canvas-postbuild.node
generated
vendored
Binary file not shown.
BIN
node_modules/canvas/build/Release/obj.target/canvas.node
generated
vendored
BIN
node_modules/canvas/build/Release/obj.target/canvas.node
generated
vendored
Binary file not shown.
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/Backends.o
generated
vendored
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/Backends.o
generated
vendored
Binary file not shown.
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/Canvas.o
generated
vendored
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/Canvas.o
generated
vendored
Binary file not shown.
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/CanvasGradient.o
generated
vendored
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/CanvasGradient.o
generated
vendored
Binary file not shown.
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/CanvasPattern.o
generated
vendored
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/CanvasPattern.o
generated
vendored
Binary file not shown.
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/CanvasRenderingContext2d.o
generated
vendored
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/CanvasRenderingContext2d.o
generated
vendored
Binary file not shown.
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/Image.o
generated
vendored
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/Image.o
generated
vendored
Binary file not shown.
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/ImageData.o
generated
vendored
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/ImageData.o
generated
vendored
Binary file not shown.
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/backend/Backend.o
generated
vendored
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/backend/Backend.o
generated
vendored
Binary file not shown.
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/backend/ImageBackend.o
generated
vendored
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/backend/ImageBackend.o
generated
vendored
Binary file not shown.
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/backend/PdfBackend.o
generated
vendored
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/backend/PdfBackend.o
generated
vendored
Binary file not shown.
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/backend/SvgBackend.o
generated
vendored
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/backend/SvgBackend.o
generated
vendored
Binary file not shown.
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/bmp/BMPParser.o
generated
vendored
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/bmp/BMPParser.o
generated
vendored
Binary file not shown.
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/closure.o
generated
vendored
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/closure.o
generated
vendored
Binary file not shown.
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/color.o
generated
vendored
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/color.o
generated
vendored
Binary file not shown.
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/init.o
generated
vendored
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/init.o
generated
vendored
Binary file not shown.
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/register_font.o
generated
vendored
BIN
node_modules/canvas/build/Release/obj.target/canvas/src/register_font.o
generated
vendored
Binary file not shown.
4
node_modules/canvas/build/config.gypi
generated
vendored
4
node_modules/canvas/build/config.gypi
generated
vendored
@@ -419,9 +419,9 @@
|
|||||||
"node_gyp": "/usr/lib/node_modules/node-gyp/bin/node-gyp.js",
|
"node_gyp": "/usr/lib/node_modules/node-gyp/bin/node-gyp.js",
|
||||||
"target": "28.3.3",
|
"target": "28.3.3",
|
||||||
"platform": "linux",
|
"platform": "linux",
|
||||||
"user_agent": "npm/11.7.0 node/v25.2.1 linux x64 workspaces/false",
|
"user_agent": "npm/11.10.1 node/v25.6.1 linux x64 workspaces/false",
|
||||||
"prefix": "/usr",
|
"prefix": "/usr",
|
||||||
"npm_version": "11.7.0",
|
"npm_version": "11.10.1",
|
||||||
"runtime": "electron",
|
"runtime": "electron",
|
||||||
"target_platform": "linux",
|
"target_platform": "linux",
|
||||||
"init_module": "/home/erik/.npm-init.js",
|
"init_module": "/home/erik/.npm-init.js",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"author": "Erik",
|
"author": "Erik",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@elgato-stream-deck/node": "^7.5.2",
|
||||||
"canvas": "^2.11.2",
|
"canvas": "^2.11.2",
|
||||||
"loupedeck": "^7.0.3"
|
"loupedeck": "^7.0.3"
|
||||||
},
|
},
|
||||||
@@ -32,7 +33,9 @@
|
|||||||
"asarUnpack": [
|
"asarUnpack": [
|
||||||
"**/*.node",
|
"**/*.node",
|
||||||
"**/canvas/**",
|
"**/canvas/**",
|
||||||
"**/loupedeck/**"
|
"**/loupedeck/**",
|
||||||
|
"**/@elgato-stream-deck/**",
|
||||||
|
"**/node-hid/**"
|
||||||
],
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"main.js",
|
"main.js",
|
||||||
@@ -40,6 +43,7 @@
|
|||||||
"src/**/*",
|
"src/**/*",
|
||||||
"assets/**/*",
|
"assets/**/*",
|
||||||
"config/**/*",
|
"config/**/*",
|
||||||
|
"udev/**/*",
|
||||||
"package.json",
|
"package.json",
|
||||||
"node_modules/**/*"
|
"node_modules/**/*"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"Bash(done)",
|
|
||||||
"Bash(python3:*)",
|
|
||||||
"Bash(grep:*)"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
vendor/
|
|
||||||
public/uploads/expenses/
|
|
||||||
public/uploads/invoices/
|
|
||||||
.git/
|
|
||||||
118
pirp/CLAUDE.md
118
pirp/CLAUDE.md
@@ -1,118 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
PIRP (Packed Internes Rechnungsprogramm) is a simple internal invoicing application written in PHP. It's a German-language business tool for managing invoices, customers, expenses, recurring subscriptions, and generating EÜR (Einnahmen-Überschuss-Rechnung / income-expenditure accounting) reports.
|
|
||||||
|
|
||||||
## Tech Stack
|
|
||||||
|
|
||||||
- **PHP** (vanilla, no framework)
|
|
||||||
- **PostgreSQL** database
|
|
||||||
- **Dompdf** for PDF invoice generation
|
|
||||||
- **Session-based authentication**
|
|
||||||
- **SMF-style UI theme** (CSS with gradients, tabs)
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
/var/www/pirp/
|
|
||||||
├── public/ # Web-accessible files (document root)
|
|
||||||
│ ├── assets/ # CSS (style.css - SMF-style theme)
|
|
||||||
│ ├── uploads/ # User-uploaded files
|
|
||||||
│ │ ├── logos/ # Company logos
|
|
||||||
│ │ ├── expenses/# Expense receipts (PDFs)
|
|
||||||
│ │ └── invoices/# Archived invoice PDFs (GoBD-compliant)
|
|
||||||
│ └── *.php # Page controllers
|
|
||||||
├── src/ # Business logic (not web-accessible)
|
|
||||||
│ ├── config.php # Database credentials, BASE_URL, session setup
|
|
||||||
│ ├── db.php # PDO connection singleton (get_db())
|
|
||||||
│ ├── auth.php # Login/logout/require_login functions
|
|
||||||
│ ├── invoice_functions.php # Settings + invoice number generation
|
|
||||||
│ ├── customer_functions.php # Customer CRUD
|
|
||||||
│ ├── expense_functions.php # Expense CRUD
|
|
||||||
│ ├── pdf_functions.php # GoBD-compliant PDF archiving
|
|
||||||
│ └── recurring_functions.php # Subscription invoice management
|
|
||||||
├── tools/ # CLI utilities
|
|
||||||
│ ├── hash.php # Password hash generator
|
|
||||||
│ ├── migrate_pdf.sql # DB migration for PDF archiving
|
|
||||||
│ ├── migrate_pdfs.php # Migrate existing invoices to archived PDFs
|
|
||||||
│ └── migrate_recurring.sql # DB migration for subscriptions
|
|
||||||
├── schema.sql # Full database schema
|
|
||||||
└── vendor/ # Composer dependencies
|
|
||||||
```
|
|
||||||
|
|
||||||
## Database
|
|
||||||
|
|
||||||
PostgreSQL with tables: `users`, `settings`, `customers`, `invoices`, `invoice_items`, `expenses`, `recurring_templates`, `recurring_template_items`, `recurring_log`. See `schema.sql` for complete schema.
|
|
||||||
|
|
||||||
Key relationships:
|
|
||||||
- `invoices` → `customers` (customer_id foreign key)
|
|
||||||
- `invoice_items` → `invoices` (invoice_id foreign key, CASCADE delete)
|
|
||||||
- `recurring_templates` → `customers`
|
|
||||||
- `recurring_template_items` → `recurring_templates` (CASCADE delete)
|
|
||||||
- `recurring_log` → `recurring_templates`, `invoices`
|
|
||||||
|
|
||||||
VAT modes: `klein` (Kleinunternehmer - VAT exempt) or `normal` (standard VAT).
|
|
||||||
|
|
||||||
## Development Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install dependencies
|
|
||||||
composer install
|
|
||||||
|
|
||||||
# Generate password hash for new user
|
|
||||||
php tools/hash.php YOUR_PASSWORD
|
|
||||||
|
|
||||||
# Initialize database (fresh install)
|
|
||||||
psql -U pirp_user -d pirp -f schema.sql
|
|
||||||
|
|
||||||
# Apply migrations (existing database)
|
|
||||||
psql -U pirp_user -d pirp -f tools/migrate_pdf.sql
|
|
||||||
psql -U pirp_user -d pirp -f tools/migrate_recurring.sql
|
|
||||||
|
|
||||||
# Migrate existing invoices to archived PDFs
|
|
||||||
php tools/migrate_pdfs.php
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Database connection in `src/config.php`. Set `BASE_URL` if running in a subdirectory (e.g., `/pirp`).
|
|
||||||
|
|
||||||
## Key Patterns
|
|
||||||
|
|
||||||
- All public pages include `require_login()` from `src/auth.php`
|
|
||||||
- Database access via `get_db()` singleton returning PDO instance
|
|
||||||
- Invoice numbers: `PIRP-YYYY-NNNNN` (auto-generated)
|
|
||||||
- Customer numbers: `PIKN-NNNNNN` (auto-generated)
|
|
||||||
|
|
||||||
## GoBD-Compliant PDF Archiving
|
|
||||||
|
|
||||||
German tax law (GoBD) requires invoices to be stored immutably. PDFs are:
|
|
||||||
- Generated once at invoice creation
|
|
||||||
- Stored in `public/uploads/invoices/{year}/`
|
|
||||||
- Protected with chmod 444
|
|
||||||
- Verified via SHA-256 hash stored in `invoices.pdf_hash`
|
|
||||||
|
|
||||||
Key functions in `src/pdf_functions.php`:
|
|
||||||
- `archive_invoice_pdf($id)` - Generate and store PDF
|
|
||||||
- `get_archived_pdf_path($id)` - Get path to archived PDF
|
|
||||||
- `verify_invoice_pdf($id)` - Verify integrity via hash
|
|
||||||
|
|
||||||
## Recurring Invoices (Subscriptions)
|
|
||||||
|
|
||||||
Subscription invoices for recurring customers with intervals:
|
|
||||||
- `monthly` - Every month
|
|
||||||
- `quarterly` - Every 3 months
|
|
||||||
- `yearly` - Every year
|
|
||||||
|
|
||||||
Key pages:
|
|
||||||
- `recurring.php` - Overview of subscription templates
|
|
||||||
- `recurring_edit.php` - Create/edit templates
|
|
||||||
- `recurring_generate.php` - Generate due invoices
|
|
||||||
|
|
||||||
Key functions in `src/recurring_functions.php`:
|
|
||||||
- `get_pending_recurring_invoices()` - Get due subscriptions
|
|
||||||
- `generate_invoice_from_template($id)` - Create invoice from template
|
|
||||||
- `calculate_next_due_date($interval, $date)` - Calculate next due date
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
FROM php:8.3-cli
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends \
|
|
||||||
libpq-dev libpng-dev libjpeg-dev libfreetype6-dev \
|
|
||||||
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
|
|
||||||
&& docker-php-ext-install pdo_pgsql gd \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
CMD ["php", "-S", "0.0.0.0:8080", "-t", "public"]
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "pirp/packed-internes-rechnungsprogramm",
|
|
||||||
"description": "Einfaches internes Rechnungsprogramm (PIRP)",
|
|
||||||
"type": "project",
|
|
||||||
"require": {
|
|
||||||
"dompdf/dompdf": "^2.0"
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"": "src/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
304
pirp/composer.lock
generated
304
pirp/composer.lock
generated
@@ -1,304 +0,0 @@
|
|||||||
{
|
|
||||||
"_readme": [
|
|
||||||
"This file locks the dependencies of your project to a known state",
|
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
|
||||||
"This file is @generated automatically"
|
|
||||||
],
|
|
||||||
"content-hash": "76bd9531733da7ca24f4b785b8fe430d",
|
|
||||||
"packages": [
|
|
||||||
{
|
|
||||||
"name": "dompdf/dompdf",
|
|
||||||
"version": "v2.0.8",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/dompdf/dompdf.git",
|
|
||||||
"reference": "c20247574601700e1f7c8dab39310fca1964dc52"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/dompdf/dompdf/zipball/c20247574601700e1f7c8dab39310fca1964dc52",
|
|
||||||
"reference": "c20247574601700e1f7c8dab39310fca1964dc52",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"ext-dom": "*",
|
|
||||||
"ext-mbstring": "*",
|
|
||||||
"masterminds/html5": "^2.0",
|
|
||||||
"phenx/php-font-lib": ">=0.5.4 <1.0.0",
|
|
||||||
"phenx/php-svg-lib": ">=0.5.2 <1.0.0",
|
|
||||||
"php": "^7.1 || ^8.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"ext-json": "*",
|
|
||||||
"ext-zip": "*",
|
|
||||||
"mockery/mockery": "^1.3",
|
|
||||||
"phpunit/phpunit": "^7.5 || ^8 || ^9",
|
|
||||||
"squizlabs/php_codesniffer": "^3.5"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"ext-gd": "Needed to process images",
|
|
||||||
"ext-gmagick": "Improves image processing performance",
|
|
||||||
"ext-imagick": "Improves image processing performance",
|
|
||||||
"ext-zlib": "Needed for pdf stream compression"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Dompdf\\": "src/"
|
|
||||||
},
|
|
||||||
"classmap": [
|
|
||||||
"lib/"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"LGPL-2.1"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "The Dompdf Community",
|
|
||||||
"homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
|
|
||||||
"homepage": "https://github.com/dompdf/dompdf",
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/dompdf/dompdf/issues",
|
|
||||||
"source": "https://github.com/dompdf/dompdf/tree/v2.0.8"
|
|
||||||
},
|
|
||||||
"time": "2024-04-29T13:06:17+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "masterminds/html5",
|
|
||||||
"version": "2.10.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/Masterminds/html5-php.git",
|
|
||||||
"reference": "fcf91eb64359852f00d921887b219479b4f21251"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251",
|
|
||||||
"reference": "fcf91eb64359852f00d921887b219479b4f21251",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"ext-dom": "*",
|
|
||||||
"php": ">=5.3.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"extra": {
|
|
||||||
"branch-alias": {
|
|
||||||
"dev-master": "2.7-dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Masterminds\\": "src"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Matt Butcher",
|
|
||||||
"email": "technosophos@gmail.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Matt Farina",
|
|
||||||
"email": "matt@mattfarina.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Asmir Mustafic",
|
|
||||||
"email": "goetas@gmail.com"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "An HTML5 parser and serializer.",
|
|
||||||
"homepage": "http://masterminds.github.io/html5-php",
|
|
||||||
"keywords": [
|
|
||||||
"HTML5",
|
|
||||||
"dom",
|
|
||||||
"html",
|
|
||||||
"parser",
|
|
||||||
"querypath",
|
|
||||||
"serializer",
|
|
||||||
"xml"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/Masterminds/html5-php/issues",
|
|
||||||
"source": "https://github.com/Masterminds/html5-php/tree/2.10.0"
|
|
||||||
},
|
|
||||||
"time": "2025-07-25T09:04:22+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "phenx/php-font-lib",
|
|
||||||
"version": "0.5.6",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/dompdf/php-font-lib.git",
|
|
||||||
"reference": "a1681e9793040740a405ac5b189275059e2a9863"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a1681e9793040740a405ac5b189275059e2a9863",
|
|
||||||
"reference": "a1681e9793040740a405ac5b189275059e2a9863",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"ext-mbstring": "*"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"FontLib\\": "src/FontLib"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"LGPL-2.1-or-later"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Fabien Ménager",
|
|
||||||
"email": "fabien.menager@gmail.com"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "A library to read, parse, export and make subsets of different types of font files.",
|
|
||||||
"homepage": "https://github.com/PhenX/php-font-lib",
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/dompdf/php-font-lib/issues",
|
|
||||||
"source": "https://github.com/dompdf/php-font-lib/tree/0.5.6"
|
|
||||||
},
|
|
||||||
"time": "2024-01-29T14:45:26+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "phenx/php-svg-lib",
|
|
||||||
"version": "0.5.4",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/dompdf/php-svg-lib.git",
|
|
||||||
"reference": "46b25da81613a9cf43c83b2a8c2c1bdab27df691"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/46b25da81613a9cf43c83b2a8c2c1bdab27df691",
|
|
||||||
"reference": "46b25da81613a9cf43c83b2a8c2c1bdab27df691",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"ext-mbstring": "*",
|
|
||||||
"php": "^7.1 || ^8.0",
|
|
||||||
"sabberworm/php-css-parser": "^8.4"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Svg\\": "src/Svg"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"LGPL-3.0-or-later"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Fabien Ménager",
|
|
||||||
"email": "fabien.menager@gmail.com"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "A library to read, parse and export to PDF SVG files.",
|
|
||||||
"homepage": "https://github.com/PhenX/php-svg-lib",
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/dompdf/php-svg-lib/issues",
|
|
||||||
"source": "https://github.com/dompdf/php-svg-lib/tree/0.5.4"
|
|
||||||
},
|
|
||||||
"time": "2024-04-08T12:52:34+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "sabberworm/php-css-parser",
|
|
||||||
"version": "v8.9.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/MyIntervals/PHP-CSS-Parser.git",
|
|
||||||
"reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/d8e916507b88e389e26d4ab03c904a082aa66bb9",
|
|
||||||
"reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"ext-iconv": "*",
|
|
||||||
"php": "^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.41",
|
|
||||||
"rawr/cross-data-providers": "^2.0.0"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"ext-mbstring": "for parsing UTF-8 CSS"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"extra": {
|
|
||||||
"branch-alias": {
|
|
||||||
"dev-main": "9.0.x-dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Sabberworm\\CSS\\": "src/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Raphael Schweikert"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Oliver Klee",
|
|
||||||
"email": "github@oliverklee.de"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Jake Hotson",
|
|
||||||
"email": "jake.github@qzdesign.co.uk"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Parser for CSS Files written in PHP",
|
|
||||||
"homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
|
|
||||||
"keywords": [
|
|
||||||
"css",
|
|
||||||
"parser",
|
|
||||||
"stylesheet"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues",
|
|
||||||
"source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.9.0"
|
|
||||||
},
|
|
||||||
"time": "2025-07-11T13:20:48+00:00"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"packages-dev": [],
|
|
||||||
"aliases": [],
|
|
||||||
"minimum-stability": "stable",
|
|
||||||
"stability-flags": [],
|
|
||||||
"prefer-stable": false,
|
|
||||||
"prefer-lowest": false,
|
|
||||||
"platform": [],
|
|
||||||
"platform-dev": [],
|
|
||||||
"plugin-api-version": "2.6.0"
|
|
||||||
}
|
|
||||||
31
pirp/dev.sh
31
pirp/dev.sh
@@ -1,31 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
case "${1:-start}" in
|
|
||||||
start)
|
|
||||||
echo "PIRP Dev-Umgebung starten..."
|
|
||||||
echo "App: http://localhost:8080"
|
|
||||||
echo "Ctrl+C zum Stoppen."
|
|
||||||
docker compose up --build
|
|
||||||
;;
|
|
||||||
stop)
|
|
||||||
docker compose down
|
|
||||||
echo "Gestoppt."
|
|
||||||
;;
|
|
||||||
reset-db)
|
|
||||||
echo "Datenbank wird zurückgesetzt..."
|
|
||||||
docker compose down -v
|
|
||||||
docker compose up --build
|
|
||||||
;;
|
|
||||||
logs)
|
|
||||||
docker compose logs -f
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Verwendung: ./dev.sh [start|stop|reset-db|logs]"
|
|
||||||
echo ""
|
|
||||||
echo " start App + DB starten (Standard)"
|
|
||||||
echo " stop Alles stoppen"
|
|
||||||
echo " reset-db DB-Volume löschen und neu aufsetzen"
|
|
||||||
echo " logs Logs verfolgen"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
services:
|
|
||||||
web:
|
|
||||||
build: .
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
volumes:
|
|
||||||
- .:/app
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
environment:
|
|
||||||
DB_HOST: db
|
|
||||||
DB_PORT: "5432"
|
|
||||||
DB_NAME: pirp
|
|
||||||
DB_USER: pirp_user
|
|
||||||
DB_PASS: PIRPdb2025!
|
|
||||||
|
|
||||||
db:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
environment:
|
|
||||||
POSTGRES_DB: pirp
|
|
||||||
POSTGRES_USER: pirp_user
|
|
||||||
POSTGRES_PASSWORD: PIRPdb2025!
|
|
||||||
volumes:
|
|
||||||
- pgdata:/var/lib/postgresql/data
|
|
||||||
- ./schema.sql:/docker-entrypoint-initdb.d/01-schema.sql
|
|
||||||
- ./tools/migrate_journal.sql:/docker-entrypoint-initdb.d/02-journal.sql
|
|
||||||
- ./tools/seed_dev.sql:/docker-entrypoint-initdb.d/03-seed.sql
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U pirp_user -d pirp"]
|
|
||||||
interval: 2s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 10
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
pgdata:
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
/**
|
|
||||||
* PirpCombobox - Tippbare Dropdown-Auswahl als Ersatz für <select>
|
|
||||||
*
|
|
||||||
* Verwendung:
|
|
||||||
* new PirpCombobox(containerEl, accountOptions, {
|
|
||||||
* placeholder: '--Konto--',
|
|
||||||
* selectedValue: 'kasse_s|soll||',
|
|
||||||
* onSelect: function(opt) { ... }
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* accountOptions: { "Gruppenname": [ {value, label, side, rev_id, exp_id}, ... ], ... }
|
|
||||||
*/
|
|
||||||
class PirpCombobox {
|
|
||||||
constructor(container, options, config) {
|
|
||||||
this.container = container;
|
|
||||||
this.options = options; // grouped: { group: [{value, label, side, rev_id, exp_id}] }
|
|
||||||
this.config = config || {};
|
|
||||||
this.flatOptions = [];
|
|
||||||
this.highlighted = -1;
|
|
||||||
|
|
||||||
// Flatten options for filtering
|
|
||||||
for (const [group, opts] of Object.entries(this.options)) {
|
|
||||||
for (const opt of opts) {
|
|
||||||
this.flatOptions.push({ group, ...opt });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.build();
|
|
||||||
this.bindEvents();
|
|
||||||
|
|
||||||
// Set initial selection
|
|
||||||
if (this.config.selectedValue) {
|
|
||||||
const parts = this.config.selectedValue.split('|');
|
|
||||||
const match = this.flatOptions.find(o =>
|
|
||||||
o.value === parts[0] && o.side === parts[1] &&
|
|
||||||
String(o.rev_id) === String(parts[2]) &&
|
|
||||||
String(o.exp_id) === String(parts[3])
|
|
||||||
);
|
|
||||||
if (match) {
|
|
||||||
this.input.value = match.label;
|
|
||||||
this.selectedOpt = match;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
build() {
|
|
||||||
this.container.classList.add('pirp-combobox');
|
|
||||||
|
|
||||||
this.input = document.createElement('input');
|
|
||||||
this.input.type = 'text';
|
|
||||||
this.input.placeholder = this.config.placeholder || '--Konto--';
|
|
||||||
this.input.autocomplete = 'off';
|
|
||||||
|
|
||||||
this.dropdown = document.createElement('div');
|
|
||||||
this.dropdown.className = 'pirp-combobox-dropdown';
|
|
||||||
this.dropdown.style.display = 'none';
|
|
||||||
|
|
||||||
this.container.appendChild(this.input);
|
|
||||||
this.container.appendChild(this.dropdown);
|
|
||||||
|
|
||||||
this.selectedOpt = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderDropdown(filter) {
|
|
||||||
this.dropdown.innerHTML = '';
|
|
||||||
this.highlighted = -1;
|
|
||||||
const f = (filter || '').toLowerCase();
|
|
||||||
let visibleItems = [];
|
|
||||||
let lastGroup = null;
|
|
||||||
|
|
||||||
for (const opt of this.flatOptions) {
|
|
||||||
if (f && opt.label.toLowerCase().indexOf(f) === -1) continue;
|
|
||||||
if (opt.group !== lastGroup) {
|
|
||||||
lastGroup = opt.group;
|
|
||||||
const groupEl = document.createElement('div');
|
|
||||||
groupEl.className = 'pirp-combobox-group';
|
|
||||||
groupEl.textContent = opt.group;
|
|
||||||
this.dropdown.appendChild(groupEl);
|
|
||||||
}
|
|
||||||
const optEl = document.createElement('div');
|
|
||||||
optEl.className = 'pirp-combobox-option';
|
|
||||||
optEl.textContent = opt.label;
|
|
||||||
optEl.dataset.index = visibleItems.length;
|
|
||||||
visibleItems.push({ el: optEl, opt });
|
|
||||||
this.dropdown.appendChild(optEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.visibleItems = visibleItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
open() {
|
|
||||||
this.renderDropdown(this.input.value === (this.selectedOpt ? this.selectedOpt.label : '') ? '' : this.input.value);
|
|
||||||
this.dropdown.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.dropdown.style.display = 'none';
|
|
||||||
this.highlighted = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
select(opt) {
|
|
||||||
this.selectedOpt = opt;
|
|
||||||
this.input.value = opt.label;
|
|
||||||
this.close();
|
|
||||||
if (this.config.onSelect) {
|
|
||||||
this.config.onSelect(opt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
highlightIndex(idx) {
|
|
||||||
if (!this.visibleItems || !this.visibleItems.length) return;
|
|
||||||
if (this.highlighted >= 0 && this.highlighted < this.visibleItems.length) {
|
|
||||||
this.visibleItems[this.highlighted].el.classList.remove('highlighted');
|
|
||||||
}
|
|
||||||
this.highlighted = Math.max(0, Math.min(idx, this.visibleItems.length - 1));
|
|
||||||
this.visibleItems[this.highlighted].el.classList.add('highlighted');
|
|
||||||
this.visibleItems[this.highlighted].el.scrollIntoView({ block: 'nearest' });
|
|
||||||
}
|
|
||||||
|
|
||||||
bindEvents() {
|
|
||||||
this.input.addEventListener('focus', () => {
|
|
||||||
this.open();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.input.addEventListener('input', () => {
|
|
||||||
this.renderDropdown(this.input.value);
|
|
||||||
this.dropdown.style.display = 'block';
|
|
||||||
if (this.visibleItems.length > 0) {
|
|
||||||
this.highlightIndex(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.input.addEventListener('keydown', (e) => {
|
|
||||||
if (this.dropdown.style.display === 'none') {
|
|
||||||
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
||||||
this.open();
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === 'ArrowDown') {
|
|
||||||
e.preventDefault();
|
|
||||||
this.highlightIndex(this.highlighted + 1);
|
|
||||||
} else if (e.key === 'ArrowUp') {
|
|
||||||
e.preventDefault();
|
|
||||||
this.highlightIndex(this.highlighted - 1);
|
|
||||||
} else if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
if (this.highlighted >= 0 && this.visibleItems[this.highlighted]) {
|
|
||||||
this.select(this.visibleItems[this.highlighted].opt);
|
|
||||||
}
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
e.preventDefault();
|
|
||||||
if (this.selectedOpt) {
|
|
||||||
this.input.value = this.selectedOpt.label;
|
|
||||||
}
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.input.addEventListener('blur', () => {
|
|
||||||
// Delay to allow click on option to register
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.selectedOpt) {
|
|
||||||
this.input.value = this.selectedOpt.label;
|
|
||||||
}
|
|
||||||
this.close();
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use mousedown to prevent blur race condition
|
|
||||||
this.dropdown.addEventListener('mousedown', (e) => {
|
|
||||||
e.preventDefault(); // Prevent blur
|
|
||||||
const optEl = e.target.closest('.pirp-combobox-option');
|
|
||||||
if (optEl) {
|
|
||||||
const idx = parseInt(optEl.dataset.index);
|
|
||||||
if (this.visibleItems[idx]) {
|
|
||||||
this.select(this.visibleItems[idx].opt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
// PIRP Command Palette (Ctrl+K / Cmd+K)
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var overlay = null;
|
|
||||||
var input = null;
|
|
||||||
var resultsList = null;
|
|
||||||
var debounceTimer = null;
|
|
||||||
var activeIndex = -1;
|
|
||||||
var currentResults = [];
|
|
||||||
|
|
||||||
var typeLabels = {
|
|
||||||
invoice: 'Rechnungen',
|
|
||||||
customer: 'Kunden',
|
|
||||||
expense: 'Ausgaben',
|
|
||||||
journal: 'Journal'
|
|
||||||
};
|
|
||||||
|
|
||||||
var typeIcons = {
|
|
||||||
invoice: '\u25B8',
|
|
||||||
customer: '\u25CF',
|
|
||||||
expense: '\u25A0',
|
|
||||||
journal: '\u25C6'
|
|
||||||
};
|
|
||||||
|
|
||||||
function create() {
|
|
||||||
overlay = document.createElement('div');
|
|
||||||
overlay.className = 'cmd-palette-overlay hidden';
|
|
||||||
overlay.addEventListener('click', function(e) {
|
|
||||||
if (e.target === overlay) close();
|
|
||||||
});
|
|
||||||
|
|
||||||
var modal = document.createElement('div');
|
|
||||||
modal.className = 'cmd-palette';
|
|
||||||
|
|
||||||
input = document.createElement('input');
|
|
||||||
input.type = 'text';
|
|
||||||
input.placeholder = 'Suche nach Rechnungen, Kunden, Ausgaben, Journal...';
|
|
||||||
input.addEventListener('input', function() {
|
|
||||||
clearTimeout(debounceTimer);
|
|
||||||
debounceTimer = setTimeout(doSearch, 200);
|
|
||||||
});
|
|
||||||
input.addEventListener('keydown', handleKeydown);
|
|
||||||
|
|
||||||
resultsList = document.createElement('div');
|
|
||||||
resultsList.className = 'cmd-palette-results';
|
|
||||||
|
|
||||||
modal.appendChild(input);
|
|
||||||
modal.appendChild(resultsList);
|
|
||||||
overlay.appendChild(modal);
|
|
||||||
document.body.appendChild(overlay);
|
|
||||||
}
|
|
||||||
|
|
||||||
function open() {
|
|
||||||
if (!overlay) create();
|
|
||||||
overlay.classList.remove('hidden');
|
|
||||||
input.value = '';
|
|
||||||
resultsList.innerHTML = '';
|
|
||||||
activeIndex = -1;
|
|
||||||
currentResults = [];
|
|
||||||
setTimeout(function() { input.focus(); }, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
if (overlay) overlay.classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
function isOpen() {
|
|
||||||
return overlay && !overlay.classList.contains('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
function doSearch() {
|
|
||||||
var q = input.value.trim();
|
|
||||||
if (q.length < 2) {
|
|
||||||
resultsList.innerHTML = '';
|
|
||||||
activeIndex = -1;
|
|
||||||
currentResults = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch('search_api.php?q=' + encodeURIComponent(q))
|
|
||||||
.then(function(r) { return r.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
if (!data.ok) return;
|
|
||||||
currentResults = data.results;
|
|
||||||
activeIndex = -1;
|
|
||||||
renderResults(data.results);
|
|
||||||
})
|
|
||||||
.catch(function() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderResults(results) {
|
|
||||||
resultsList.innerHTML = '';
|
|
||||||
if (!results.length) {
|
|
||||||
var empty = document.createElement('div');
|
|
||||||
empty.className = 'cmd-palette-empty';
|
|
||||||
empty.textContent = 'Keine Ergebnisse';
|
|
||||||
resultsList.appendChild(empty);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group by type
|
|
||||||
var groups = {};
|
|
||||||
var order = [];
|
|
||||||
results.forEach(function(r) {
|
|
||||||
if (!groups[r.type]) {
|
|
||||||
groups[r.type] = [];
|
|
||||||
order.push(r.type);
|
|
||||||
}
|
|
||||||
groups[r.type].push(r);
|
|
||||||
});
|
|
||||||
|
|
||||||
var idx = 0;
|
|
||||||
order.forEach(function(type) {
|
|
||||||
var header = document.createElement('div');
|
|
||||||
header.className = 'cmd-palette-group';
|
|
||||||
header.textContent = typeLabels[type] || type;
|
|
||||||
resultsList.appendChild(header);
|
|
||||||
|
|
||||||
groups[type].forEach(function(r) {
|
|
||||||
var item = document.createElement('a');
|
|
||||||
item.className = 'cmd-palette-item';
|
|
||||||
item.href = r.url;
|
|
||||||
item.setAttribute('data-idx', idx);
|
|
||||||
|
|
||||||
var icon = document.createElement('span');
|
|
||||||
icon.className = 'cmd-palette-icon';
|
|
||||||
icon.textContent = typeIcons[r.type] || '\u25B8';
|
|
||||||
item.appendChild(icon);
|
|
||||||
|
|
||||||
var text = document.createElement('span');
|
|
||||||
text.className = 'cmd-palette-text';
|
|
||||||
|
|
||||||
var title = document.createElement('span');
|
|
||||||
title.className = 'cmd-palette-title';
|
|
||||||
title.textContent = r.title;
|
|
||||||
text.appendChild(title);
|
|
||||||
|
|
||||||
if (r.subtitle) {
|
|
||||||
var sub = document.createElement('span');
|
|
||||||
sub.className = 'cmd-palette-subtitle';
|
|
||||||
sub.textContent = r.subtitle;
|
|
||||||
text.appendChild(sub);
|
|
||||||
}
|
|
||||||
|
|
||||||
item.appendChild(text);
|
|
||||||
item.addEventListener('mouseenter', function() {
|
|
||||||
setActive(parseInt(item.getAttribute('data-idx')));
|
|
||||||
});
|
|
||||||
resultsList.appendChild(item);
|
|
||||||
idx++;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setActive(idx) {
|
|
||||||
var items = resultsList.querySelectorAll('.cmd-palette-item');
|
|
||||||
items.forEach(function(el) { el.classList.remove('active'); });
|
|
||||||
activeIndex = idx;
|
|
||||||
if (idx >= 0 && idx < items.length) {
|
|
||||||
items[idx].classList.add('active');
|
|
||||||
items[idx].scrollIntoView({ block: 'nearest' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeydown(e) {
|
|
||||||
var items = resultsList.querySelectorAll('.cmd-palette-item');
|
|
||||||
var count = items.length;
|
|
||||||
|
|
||||||
if (e.key === 'ArrowDown') {
|
|
||||||
e.preventDefault();
|
|
||||||
setActive(activeIndex < count - 1 ? activeIndex + 1 : 0);
|
|
||||||
} else if (e.key === 'ArrowUp') {
|
|
||||||
e.preventDefault();
|
|
||||||
setActive(activeIndex > 0 ? activeIndex - 1 : count - 1);
|
|
||||||
} else if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
if (activeIndex >= 0 && activeIndex < count) {
|
|
||||||
window.location = items[activeIndex].href;
|
|
||||||
}
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
e.preventDefault();
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global keyboard listener
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
||||||
e.preventDefault();
|
|
||||||
if (isOpen()) {
|
|
||||||
close();
|
|
||||||
} else {
|
|
||||||
open();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (e.key === 'Escape' && isOpen()) {
|
|
||||||
e.preventDefault();
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,192 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_once __DIR__ . '/../src/icons.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
$pdo = get_db();
|
|
||||||
|
|
||||||
$filter_type = $_GET['type'] ?? 'all'; // all | rechnung | ausgabe | mahnung
|
|
||||||
$filter_from = trim($_GET['from'] ?? '');
|
|
||||||
$filter_to = trim($_GET['to'] ?? '');
|
|
||||||
$filter_q = trim($_GET['q'] ?? '');
|
|
||||||
$invoice_id = isset($_GET['invoice_id']) ? (int)$_GET['invoice_id'] : 0;
|
|
||||||
|
|
||||||
$belege = [];
|
|
||||||
|
|
||||||
// ---- Rechnungs-PDFs ----
|
|
||||||
if ($filter_type === 'all' || $filter_type === 'rechnung') {
|
|
||||||
$base = "FROM invoices i JOIN customers c ON c.id = i.customer_id WHERE i.pdf_path IS NOT NULL";
|
|
||||||
$params = [];
|
|
||||||
if ($filter_from) { $base .= " AND i.invoice_date >= :from"; $params[':from'] = $filter_from; }
|
|
||||||
if ($filter_to) { $base .= " AND i.invoice_date <= :to"; $params[':to'] = $filter_to; }
|
|
||||||
if ($filter_q) { $base .= " AND (i.invoice_number ILIKE :q OR c.name ILIKE :q2)";
|
|
||||||
$params[':q'] = '%' . $filter_q . '%'; $params[':q2'] = '%' . $filter_q . '%'; }
|
|
||||||
try {
|
|
||||||
$sql = "SELECT i.id, i.invoice_date AS beleg_date, i.invoice_number AS beleg_ref,
|
|
||||||
c.name AS kunde, i.total_gross AS betrag, i.pdf_path,
|
|
||||||
'rechnung' AS beleg_type, COALESCE(i.is_storno, FALSE) AS is_storno $base";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
} catch (\PDOException $e) {
|
|
||||||
// is_storno Spalte noch nicht migriert — Fallback ohne die Spalte
|
|
||||||
$sql = "SELECT i.id, i.invoice_date AS beleg_date, i.invoice_number AS beleg_ref,
|
|
||||||
c.name AS kunde, i.total_gross AS betrag, i.pdf_path,
|
|
||||||
'rechnung' AS beleg_type, FALSE AS is_storno $base";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
}
|
|
||||||
$belege = array_merge($belege, $stmt->fetchAll(PDO::FETCH_ASSOC));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Ausgaben-Belege ----
|
|
||||||
if ($filter_type === 'all' || $filter_type === 'ausgabe') {
|
|
||||||
$sql = "SELECT e.id, e.expense_date AS beleg_date, e.description AS beleg_ref,
|
|
||||||
'' AS kunde, e.amount AS betrag, e.attachment_path AS pdf_path,
|
|
||||||
'ausgabe' AS beleg_type, FALSE AS is_storno
|
|
||||||
FROM expenses e
|
|
||||||
WHERE e.attachment_path IS NOT NULL";
|
|
||||||
$params = [];
|
|
||||||
if ($filter_from) { $sql .= " AND e.expense_date >= :from"; $params[':from'] = $filter_from; }
|
|
||||||
if ($filter_to) { $sql .= " AND e.expense_date <= :to"; $params[':to'] = $filter_to; }
|
|
||||||
if ($filter_q) { $sql .= " AND e.description ILIKE :q"; $params[':q'] = '%' . $filter_q . '%'; }
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
$belege = array_merge($belege, $stmt->fetchAll(PDO::FETCH_ASSOC));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Mahnungen ----
|
|
||||||
$show_mahnungen = ($filter_type === 'all' || $filter_type === 'mahnung');
|
|
||||||
if ($show_mahnungen) {
|
|
||||||
$sql = "SELECT m.id, m.mahnung_date AS beleg_date,
|
|
||||||
'MAHNUNG L' || m.level || ' – ' || i.invoice_number AS beleg_ref,
|
|
||||||
c.name AS kunde, i.total_gross + m.fee_amount AS betrag,
|
|
||||||
m.pdf_path, 'mahnung' AS beleg_type, FALSE AS is_storno,
|
|
||||||
m.invoice_id
|
|
||||||
FROM mahnungen m
|
|
||||||
JOIN invoices i ON i.id = m.invoice_id
|
|
||||||
JOIN customers c ON c.id = i.customer_id
|
|
||||||
WHERE m.pdf_path IS NOT NULL";
|
|
||||||
$params = [];
|
|
||||||
if ($invoice_id) { $sql .= " AND m.invoice_id = :iid"; $params[':iid'] = $invoice_id; }
|
|
||||||
if ($filter_from) { $sql .= " AND m.mahnung_date >= :from"; $params[':from'] = $filter_from; }
|
|
||||||
if ($filter_to) { $sql .= " AND m.mahnung_date <= :to"; $params[':to'] = $filter_to; }
|
|
||||||
if ($filter_q) { $sql .= " AND (i.invoice_number ILIKE :q OR c.name ILIKE :q2)";
|
|
||||||
$params[':q'] = '%' . $filter_q . '%'; $params[':q2'] = '%' . $filter_q . '%'; }
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
$belege = array_merge($belege, $stmt->fetchAll(PDO::FETCH_ASSOC));
|
|
||||||
} catch (\PDOException $e) {
|
|
||||||
// Tabelle noch nicht migriert
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sortieren: neueste zuerst
|
|
||||||
usort($belege, fn($a, $b) => strcmp($b['beleg_date'], $a['beleg_date']));
|
|
||||||
|
|
||||||
$type_labels = ['rechnung' => 'Rechnung', 'ausgabe' => 'Ausgabe', 'mahnung' => 'Mahnung'];
|
|
||||||
$type_colors = ['rechnung' => 'var(--accent)', 'ausgabe' => 'var(--info)', 'mahnung' => 'var(--warning)'];
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Belegarchiv</title>
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>PIRP</h1>
|
|
||||||
<nav>
|
|
||||||
<a href="<?= url_for('index.php') ?>"><?= icon_dashboard() ?>Dashboard</a>
|
|
||||||
<a href="<?= url_for('invoices.php') ?>"><?= icon_invoices() ?>Rechnungen</a>
|
|
||||||
<a href="<?= url_for('customers.php') ?>"><?= icon_customers() ?>Kunden</a>
|
|
||||||
<a href="<?= url_for('expenses.php') ?>"><?= icon_expenses() ?>Ausgaben</a>
|
|
||||||
<a href="<?= url_for('belegarchiv.php') ?>" class="active"><?= icon_archive() ?>Belege</a>
|
|
||||||
<a href="<?= url_for('journal.php') ?>"><?= icon_journal() ?>Journal</a>
|
|
||||||
<a href="<?= url_for('euer.php') ?>"><?= icon_euer() ?>EÜR</a>
|
|
||||||
<a href="<?= url_for('settings.php') ?>"><?= icon_settings() ?>Einstellungen</a>
|
|
||||||
<a href="<?= url_for('logout.php') ?>"><?= icon_logout() ?>Logout (<?= htmlspecialchars($_SESSION['username'] ?? '') ?>)</a>
|
|
||||||
<span class="cmd-k-hint" onclick="document.dispatchEvent(new KeyboardEvent('keydown',{key:'k',ctrlKey:true}))"><kbd>Ctrl+K</kbd></span>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<form method="get" class="filters">
|
|
||||||
<label>Typ:
|
|
||||||
<select name="type">
|
|
||||||
<option value="all" <?= $filter_type === 'all' ? 'selected' : '' ?>>Alle</option>
|
|
||||||
<option value="rechnung" <?= $filter_type === 'rechnung' ? 'selected' : '' ?>>Rechnungen</option>
|
|
||||||
<option value="ausgabe" <?= $filter_type === 'ausgabe' ? 'selected' : '' ?>>Ausgaben</option>
|
|
||||||
<option value="mahnung" <?= $filter_type === 'mahnung' ? 'selected' : '' ?>>Mahnungen</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>Suche:
|
|
||||||
<input type="text" name="q" value="<?= htmlspecialchars($filter_q) ?>" placeholder="Nr., Beschreibung, Kunde...">
|
|
||||||
</label>
|
|
||||||
<label>Von:
|
|
||||||
<input type="date" name="from" value="<?= htmlspecialchars($filter_from) ?>">
|
|
||||||
</label>
|
|
||||||
<label>Bis:
|
|
||||||
<input type="date" name="to" value="<?= htmlspecialchars($filter_to) ?>">
|
|
||||||
</label>
|
|
||||||
<button type="submit">Filtern</button>
|
|
||||||
<a href="<?= url_for('belegarchiv.php') ?>">Zurücksetzen</a>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>Belegarchiv
|
|
||||||
<span style="font-weight:normal;font-size:12px;color:var(--text-muted);"><?= count($belege) ?> Dokument(e)</span>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<?php if (empty($belege)): ?>
|
|
||||||
<p style="color:var(--text-muted);">Keine Belege gefunden.</p>
|
|
||||||
<?php else: ?>
|
|
||||||
<table class="list">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>Typ</th>
|
|
||||||
<th>Referenz / Beschreibung</th>
|
|
||||||
<th>Kunde</th>
|
|
||||||
<th style="text-align:right;">Betrag</th>
|
|
||||||
<th>PDF</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($belege as $b): ?>
|
|
||||||
<tr>
|
|
||||||
<td><?= date('d.m.Y', strtotime($b['beleg_date'])) ?></td>
|
|
||||||
<td>
|
|
||||||
<span style="font-size:10px;color:<?= $type_colors[$b['beleg_type']] ?? 'var(--text)' ?>;">
|
|
||||||
<?= $type_labels[$b['beleg_type']] ?? $b['beleg_type'] ?>
|
|
||||||
<?php if (!empty($b['is_storno'])): ?>
|
|
||||||
<span style="color:var(--error);"> · STORNO</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td><?= htmlspecialchars($b['beleg_ref']) ?></td>
|
|
||||||
<td style="color:var(--text-muted);"><?= htmlspecialchars($b['kunde']) ?></td>
|
|
||||||
<td style="text-align:right;font-family:var(--font-mono);font-size:12px;">
|
|
||||||
<?= number_format((float)$b['betrag'], 2, ',', '.') ?> €
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<?php if ($b['beleg_type'] === 'rechnung'): ?>
|
|
||||||
<a href="<?= url_for('invoice_pdf.php?id=' . $b['id']) ?>" target="_blank">PDF</a>
|
|
||||||
<?php elseif ($b['beleg_type'] === 'ausgabe'): ?>
|
|
||||||
<a href="<?= url_for('expense_file.php?id=' . $b['id']) ?>" target="_blank">PDF</a>
|
|
||||||
<?php elseif ($b['beleg_type'] === 'mahnung'): ?>
|
|
||||||
<a href="<?= url_for('mahnung_pdf.php?id=' . $b['id']) ?>" target="_blank">PDF</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<?php endif; ?>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<script src="assets/command-palette.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/customer_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/icons.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
$action = $_GET['action'] ?? '';
|
|
||||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
|
||||||
$msg = '';
|
|
||||||
$error = '';
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$idPost = isset($_POST['id']) ? (int)$_POST['id'] : null;
|
|
||||||
$data = [
|
|
||||||
'name' => $_POST['name'] ?? '',
|
|
||||||
'address' => $_POST['address'] ?? '',
|
|
||||||
'zip' => $_POST['zip'] ?? '',
|
|
||||||
'city' => $_POST['city'] ?? '',
|
|
||||||
'country' => $_POST['country'] ?? '',
|
|
||||||
];
|
|
||||||
if (trim($data['name']) === '') {
|
|
||||||
$error = 'Name darf nicht leer sein.';
|
|
||||||
} else {
|
|
||||||
save_customer($idPost, $data);
|
|
||||||
$msg = 'Kunde gespeichert.';
|
|
||||||
$action = '';
|
|
||||||
$id = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($action === 'delete' && $id) {
|
|
||||||
delete_customer($id);
|
|
||||||
$msg = 'Kunde gelöscht.';
|
|
||||||
$action = '';
|
|
||||||
$id = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$customers = get_customers();
|
|
||||||
$editCustomer = null;
|
|
||||||
if ($action === 'edit' && $id) {
|
|
||||||
$editCustomer = get_customer($id);
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Kunden</title>
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>PIRP</h1>
|
|
||||||
<nav>
|
|
||||||
<a href="<?= url_for('index.php') ?>"><?= icon_dashboard() ?>Dashboard</a>
|
|
||||||
<a href="<?= url_for('invoices.php') ?>"><?= icon_invoices() ?>Rechnungen</a>
|
|
||||||
<a href="<?= url_for('customers.php') ?>" class="active"><?= icon_customers() ?>Kunden</a>
|
|
||||||
<a href="<?= url_for('expenses.php') ?>"><?= icon_expenses() ?>Ausgaben</a>
|
|
||||||
<a href="<?= url_for('belegarchiv.php') ?>"><?= icon_archive() ?>Belege</a>
|
|
||||||
<a href="<?= url_for('journal.php') ?>"><?= icon_journal() ?>Journal</a>
|
|
||||||
<a href="<?= url_for('euer.php') ?>"><?= icon_euer() ?>EÜR</a>
|
|
||||||
<a href="<?= url_for('settings.php') ?>"><?= icon_settings() ?>Einstellungen</a>
|
|
||||||
<a href="<?= url_for('logout.php') ?>"><?= icon_logout() ?>Logout (<?= htmlspecialchars($_SESSION['username'] ?? '') ?>)</a>
|
|
||||||
<span class="cmd-k-hint" onclick="document.dispatchEvent(new KeyboardEvent('keydown',{key:'k',ctrlKey:true}))"><kbd>Ctrl+K</kbd></span>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<?php if ($msg): ?><p class="success"><?= htmlspecialchars($msg) ?></p><?php endif; ?>
|
|
||||||
<?php if ($error): ?><p class="error"><?= htmlspecialchars($error) ?></p><?php endif; ?>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2><?= $editCustomer ? 'Kunde bearbeiten' : 'Neuer Kunde' ?></h2>
|
|
||||||
<form method="post">
|
|
||||||
<input type="hidden" name="id" value="<?= htmlspecialchars($editCustomer['id'] ?? '') ?>">
|
|
||||||
<label>Name:
|
|
||||||
<input type="text" name="name" value="<?= htmlspecialchars($editCustomer['name'] ?? '') ?>" required>
|
|
||||||
</label>
|
|
||||||
<label>Adresse:
|
|
||||||
<textarea name="address" rows="3"><?= htmlspecialchars($editCustomer['address'] ?? '') ?></textarea>
|
|
||||||
</label>
|
|
||||||
<div class="flex-row">
|
|
||||||
<label>PLZ:
|
|
||||||
<input type="text" name="zip" value="<?= htmlspecialchars($editCustomer['zip'] ?? '') ?>">
|
|
||||||
</label>
|
|
||||||
<label>Ort:
|
|
||||||
<input type="text" name="city" value="<?= htmlspecialchars($editCustomer['city'] ?? '') ?>">
|
|
||||||
</label>
|
|
||||||
<label>Land:
|
|
||||||
<input type="text" name="country" value="<?= htmlspecialchars($editCustomer['country'] ?? '') ?>">
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<?php if (!empty($editCustomer['customer_number'])): ?>
|
|
||||||
<p>Kundennummer: <?= htmlspecialchars($editCustomer['customer_number']) ?></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
<button type="submit">Speichern</button>
|
|
||||||
<?php if ($editCustomer): ?>
|
|
||||||
<a href="<?= url_for('customers.php') ?>">Abbrechen</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>Alle Kunden</h2>
|
|
||||||
<table class="list">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Kundennr.</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Adresse</th>
|
|
||||||
<th>Ort</th>
|
|
||||||
<th>Land</th>
|
|
||||||
<th>Aktion</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($customers as $c): ?>
|
|
||||||
<tr>
|
|
||||||
<td><?= htmlspecialchars($c['customer_number'] ?? '') ?></td>
|
|
||||||
<td><?= htmlspecialchars($c['name']) ?></td>
|
|
||||||
<td><?= nl2br(htmlspecialchars($c['address'])) ?></td>
|
|
||||||
<td><?= htmlspecialchars($c['zip'] . ' ' . $c['city']) ?></td>
|
|
||||||
<td><?= htmlspecialchars($c['country']) ?></td>
|
|
||||||
<td>
|
|
||||||
<a href="<?= url_for('customers.php?action=edit&id=' . $c['id']) ?>">Bearbeiten</a>
|
|
||||||
<a href="<?= url_for('customers.php?action=delete&id=' . $c['id']) ?>" onclick="return confirm('Kunde wirklich löschen?');">Löschen</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php if (empty($customers)): ?>
|
|
||||||
<tr><td colspan="6">Keine Kunden vorhanden.</td></tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<script src="assets/command-palette.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,336 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_once __DIR__ . '/../src/journal_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/icons.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
$pdo = get_db();
|
|
||||||
|
|
||||||
// Jahr bestimmen
|
|
||||||
$years = get_journal_years();
|
|
||||||
$current_cal_year = (int)date('Y');
|
|
||||||
|
|
||||||
$year = isset($_GET['year']) ? (int)$_GET['year'] : $current_cal_year;
|
|
||||||
|
|
||||||
// Journal-Jahr finden (oder automatisch erstellen bei Bedarf)
|
|
||||||
$journal_year = get_journal_year_by_year($year);
|
|
||||||
|
|
||||||
// Fehlende Buchungen nachholen (Quick-Fix)
|
|
||||||
$fix_msg = '';
|
|
||||||
$fix_errors = [];
|
|
||||||
if (isset($_GET['fix_missing']) && $journal_year) {
|
|
||||||
$fixed_inv = 0;
|
|
||||||
$fixed_exp = 0;
|
|
||||||
$consistency = check_journal_consistency();
|
|
||||||
|
|
||||||
foreach ($consistency['unbooked_invoice_list'] as $inv) {
|
|
||||||
try {
|
|
||||||
create_journal_entry_from_invoice((int)$inv['id']);
|
|
||||||
$fixed_inv++;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$fix_errors[] = 'Rechnung ' . ($inv['invoice_number'] ?? '#' . $inv['id']) . ': ' . $e->getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($consistency['unbooked_expense_list'] as $exp) {
|
|
||||||
try {
|
|
||||||
create_journal_entry_from_expense((int)$exp['id']);
|
|
||||||
$fixed_exp++;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$fix_errors[] = 'Ausgabe "' . ($exp['description'] ?? '#' . $exp['id']) . '": ' . $e->getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$fix_msg = $fixed_inv . ' Rechnungen und ' . $fixed_exp . ' Ausgaben nachgebucht.';
|
|
||||||
if ($fix_errors) {
|
|
||||||
$fix_msg .= ' ' . count($fix_errors) . ' Fehler aufgetreten.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CSV-Export Journal
|
|
||||||
if (isset($_GET['csv_journal']) && $journal_year) {
|
|
||||||
$euer = generate_journal_euer((int)$journal_year['id']);
|
|
||||||
|
|
||||||
header('Content-Type: text/csv; charset=utf-8');
|
|
||||||
header('Content-Disposition: attachment; filename="eur_' . $year . '.csv"');
|
|
||||||
|
|
||||||
$out = fopen('php://output', 'w');
|
|
||||||
fwrite($out, "\xEF\xBB\xBF");
|
|
||||||
|
|
||||||
fputcsv($out, ['EÜR - ' . $year], ';');
|
|
||||||
fputcsv($out, [], ';');
|
|
||||||
|
|
||||||
fputcsv($out, ['Position', 'Betrag'], ';');
|
|
||||||
fputcsv($out, [], ';');
|
|
||||||
|
|
||||||
fputcsv($out, ['EINNAHMEN'], ';');
|
|
||||||
foreach ($euer['erloese_detail'] as $row) {
|
|
||||||
fputcsv($out, [$row['name'], number_format((float)$row['total'], 2, ',', '.')], ';');
|
|
||||||
}
|
|
||||||
if ($euer['sonstiges_einnahmen'] > 0) {
|
|
||||||
fputcsv($out, ['Sonstige Einnahmen', number_format($euer['sonstiges_einnahmen'], 2, ',', '.')], ';');
|
|
||||||
}
|
|
||||||
fputcsv($out, ['Einnahmen gesamt', number_format($euer['einnahmen_total'], 2, ',', '.')], ';');
|
|
||||||
fputcsv($out, [], ';');
|
|
||||||
|
|
||||||
fputcsv($out, ['AUSGABEN'], ';');
|
|
||||||
if ($euer['wareneingang'] > 0) {
|
|
||||||
fputcsv($out, ['Wareneingang', number_format($euer['wareneingang'], 2, ',', '.')], ';');
|
|
||||||
}
|
|
||||||
foreach ($euer['aufwand_detail'] as $row) {
|
|
||||||
fputcsv($out, [$row['name'], number_format((float)$row['total'], 2, ',', '.')], ';');
|
|
||||||
}
|
|
||||||
if ($euer['sonstiges_ausgaben'] > 0) {
|
|
||||||
fputcsv($out, ['Sonstige Ausgaben', number_format($euer['sonstiges_ausgaben'], 2, ',', '.')], ';');
|
|
||||||
}
|
|
||||||
fputcsv($out, ['Ausgaben gesamt', number_format($euer['ausgaben_total'], 2, ',', '.')], ';');
|
|
||||||
fputcsv($out, [], ';');
|
|
||||||
|
|
||||||
fputcsv($out, ['STEUER'], ';');
|
|
||||||
fputcsv($out, ['MwSt (eingenommen)', number_format($euer['mwst'], 2, ',', '.')], ';');
|
|
||||||
fputcsv($out, ['VorSt (gezahlt)', number_format($euer['vorst'], 2, ',', '.')], ';');
|
|
||||||
fputcsv($out, ['Steuer-Saldo', number_format($euer['steuer_saldo'], 2, ',', '.')], ';');
|
|
||||||
fputcsv($out, [], ';');
|
|
||||||
|
|
||||||
if ($euer['privat_entnahmen'] > 0 || $euer['privat_einlagen'] > 0) {
|
|
||||||
fputcsv($out, ['PRIVATKONTEN'], ';');
|
|
||||||
if ($euer['privat_einlagen'] > 0) {
|
|
||||||
fputcsv($out, ['Privateinlagen', number_format($euer['privat_einlagen'], 2, ',', '.')], ';');
|
|
||||||
}
|
|
||||||
if ($euer['privat_entnahmen'] > 0) {
|
|
||||||
fputcsv($out, ['Privatentnahmen', number_format($euer['privat_entnahmen'], 2, ',', '.')], ';');
|
|
||||||
}
|
|
||||||
fputcsv($out, [], ';');
|
|
||||||
}
|
|
||||||
|
|
||||||
fputcsv($out, ['GEWINN', number_format($euer['gewinn'], 2, ',', '.')], ';');
|
|
||||||
|
|
||||||
fclose($out);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Journal-Daten laden
|
|
||||||
$journal_euer = null;
|
|
||||||
if ($journal_year) {
|
|
||||||
$journal_euer = generate_journal_euer((int)$journal_year['id']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Konsistenz-Check: Fehlende Buchungen
|
|
||||||
$consistency = check_journal_consistency();
|
|
||||||
$has_missing = ($consistency['unbooked_invoices'] > 0 || $consistency['unbooked_expenses'] > 0);
|
|
||||||
|
|
||||||
// Verfügbare Jahre sammeln (aus Journal + Rechnungen + Ausgaben)
|
|
||||||
$available_years = [];
|
|
||||||
foreach ($years as $y) {
|
|
||||||
$available_years[(int)$y['year']] = true;
|
|
||||||
}
|
|
||||||
$stmt = $pdo->query("SELECT DISTINCT EXTRACT(YEAR FROM invoice_date)::int AS y FROM invoices WHERE paid = TRUE ORDER BY y DESC");
|
|
||||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|
||||||
$available_years[$row['y']] = true;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT DISTINCT EXTRACT(YEAR FROM expense_date)::int AS y FROM expenses WHERE paid = TRUE ORDER BY y DESC");
|
|
||||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|
||||||
$available_years[$row['y']] = true;
|
|
||||||
}
|
|
||||||
} catch (PDOException $e) {}
|
|
||||||
krsort($available_years);
|
|
||||||
$available_years = array_keys($available_years);
|
|
||||||
if (empty($available_years)) {
|
|
||||||
$available_years = [$current_cal_year];
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>EÜR <?= $year ?></title>
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>PIRP</h1>
|
|
||||||
<nav>
|
|
||||||
<a href="<?= url_for('index.php') ?>"><?= icon_dashboard() ?>Dashboard</a>
|
|
||||||
<a href="<?= url_for('invoices.php') ?>"><?= icon_invoices() ?>Rechnungen</a>
|
|
||||||
<a href="<?= url_for('customers.php') ?>"><?= icon_customers() ?>Kunden</a>
|
|
||||||
<a href="<?= url_for('expenses.php') ?>"><?= icon_expenses() ?>Ausgaben</a>
|
|
||||||
<a href="<?= url_for('belegarchiv.php') ?>"><?= icon_archive() ?>Belege</a>
|
|
||||||
<a href="<?= url_for('journal.php') ?>"><?= icon_journal() ?>Journal</a>
|
|
||||||
<a href="<?= url_for('euer.php') ?>" class="active"><?= icon_euer() ?>EÜR</a>
|
|
||||||
<a href="<?= url_for('settings.php') ?>"><?= icon_settings() ?>Einstellungen</a>
|
|
||||||
<a href="<?= url_for('logout.php') ?>"><?= icon_logout() ?>Logout (<?= htmlspecialchars($_SESSION['username'] ?? '') ?>)</a>
|
|
||||||
<span class="cmd-k-hint" onclick="document.dispatchEvent(new KeyboardEvent('keydown',{key:'k',ctrlKey:true}))"><kbd>Ctrl+K</kbd></span>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<h2>Einnahmen-Überschuss-Rechnung <?= $year ?></h2>
|
|
||||||
|
|
||||||
<!-- Jahr-Auswahl -->
|
|
||||||
<form method="get" style="margin-bottom:16px;">
|
|
||||||
<div class="flex-row">
|
|
||||||
<label>Jahr:
|
|
||||||
<select name="year" onchange="this.form.submit();">
|
|
||||||
<?php foreach ($available_years as $y): ?>
|
|
||||||
<option value="<?= $y ?>" <?= $y == $year ? 'selected' : '' ?>><?= $y ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<?php if ($fix_msg): ?>
|
|
||||||
<p class="success"><?= htmlspecialchars($fix_msg) ?></p>
|
|
||||||
<?php if (!empty($fix_errors)): ?>
|
|
||||||
<div class="gobd-warning">
|
|
||||||
<h3>Fehler beim Nachbuchen</h3>
|
|
||||||
<ul>
|
|
||||||
<?php foreach ($fix_errors as $fe): ?>
|
|
||||||
<li><?= htmlspecialchars($fe) ?></li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
<p style="font-size:11px;">Bitte die betroffenen Belege manuell prüfen und ggf. eine Aufwandskategorie zuweisen.</p>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($has_missing): ?>
|
|
||||||
<div class="gobd-warning">
|
|
||||||
<h3>Fehlende Journalbuchungen</h3>
|
|
||||||
<p>Es gibt bezahlte Belege ohne Journaleintrag. Die EÜR ist dadurch unvollständig.</p>
|
|
||||||
<ul>
|
|
||||||
<?php if ($consistency['unbooked_invoices'] > 0): ?>
|
|
||||||
<li><strong><?= $consistency['unbooked_invoices'] ?></strong> bezahlte Rechnung(en) ohne Journalbuchung</li>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($consistency['unbooked_expenses'] > 0): ?>
|
|
||||||
<li><strong><?= $consistency['unbooked_expenses'] ?></strong> bezahlte Ausgabe(n) ohne Journalbuchung</li>
|
|
||||||
<?php endif; ?>
|
|
||||||
</ul>
|
|
||||||
<a href="<?= url_for('euer.php?year=' . $year . '&fix_missing=1') ?>"
|
|
||||||
class="button" onclick="return confirm('Fehlende Journalbuchungen jetzt automatisch erstellen?');">
|
|
||||||
Fehlende Buchungen nachholen
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($journal_euer): ?>
|
|
||||||
<section class="euer-section">
|
|
||||||
<p class="euer-desc">Basierend auf Journalbuchungen (Zufluss-/Abflussprinzip)</p>
|
|
||||||
|
|
||||||
<h4>Einnahmen</h4>
|
|
||||||
<table class="list">
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($journal_euer['erloese_detail'] as $row): ?>
|
|
||||||
<tr>
|
|
||||||
<td><?= htmlspecialchars($row['name']) ?></td>
|
|
||||||
<td style="text-align:right;"><?= number_format((float)$row['total'], 2, ',', '.') ?> €</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php if ($journal_euer['sonstiges_einnahmen'] > 0): ?>
|
|
||||||
<tr>
|
|
||||||
<td>Sonstige Einnahmen</td>
|
|
||||||
<td style="text-align:right;"><?= number_format($journal_euer['sonstiges_einnahmen'], 2, ',', '.') ?> €</td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
<tr>
|
|
||||||
<td><strong>Einnahmen gesamt</strong></td>
|
|
||||||
<td style="text-align:right;"><strong><?= number_format($journal_euer['einnahmen_total'], 2, ',', '.') ?> €</strong></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<h4>Ausgaben</h4>
|
|
||||||
<table class="list">
|
|
||||||
<tbody>
|
|
||||||
<?php if ($journal_euer['wareneingang'] > 0): ?>
|
|
||||||
<tr>
|
|
||||||
<td>Wareneingang</td>
|
|
||||||
<td style="text-align:right;"><?= number_format($journal_euer['wareneingang'], 2, ',', '.') ?> €</td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php foreach ($journal_euer['aufwand_detail'] as $row): ?>
|
|
||||||
<tr>
|
|
||||||
<td><?= htmlspecialchars($row['name']) ?></td>
|
|
||||||
<td style="text-align:right;"><?= number_format((float)$row['total'], 2, ',', '.') ?> €</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php if ($journal_euer['sonstiges_ausgaben'] > 0): ?>
|
|
||||||
<tr>
|
|
||||||
<td>Sonstige Ausgaben</td>
|
|
||||||
<td style="text-align:right;"><?= number_format($journal_euer['sonstiges_ausgaben'], 2, ',', '.') ?> €</td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
<tr>
|
|
||||||
<td><strong>Ausgaben gesamt</strong></td>
|
|
||||||
<td style="text-align:right;"><strong><?= number_format($journal_euer['ausgaben_total'], 2, ',', '.') ?> €</strong></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<h4>Steuer</h4>
|
|
||||||
<table class="list">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>MwSt (eingenommen)</td>
|
|
||||||
<td style="text-align:right;"><?= number_format($journal_euer['mwst'], 2, ',', '.') ?> €</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>VorSt (gezahlt)</td>
|
|
||||||
<td style="text-align:right;"><?= number_format($journal_euer['vorst'], 2, ',', '.') ?> €</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><strong>Steuer-Saldo</strong></td>
|
|
||||||
<td style="text-align:right;"><strong><?= number_format($journal_euer['steuer_saldo'], 2, ',', '.') ?> €</strong></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<?php if ($journal_euer['privat_entnahmen'] > 0 || $journal_euer['privat_einlagen'] > 0): ?>
|
|
||||||
<h4>Privatkonten</h4>
|
|
||||||
<table class="list">
|
|
||||||
<tbody>
|
|
||||||
<?php if ($journal_euer['privat_einlagen'] > 0): ?>
|
|
||||||
<tr>
|
|
||||||
<td>Privateinlagen</td>
|
|
||||||
<td style="text-align:right;"><?= number_format($journal_euer['privat_einlagen'], 2, ',', '.') ?> €</td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($journal_euer['privat_entnahmen'] > 0): ?>
|
|
||||||
<tr>
|
|
||||||
<td>Privatentnahmen</td>
|
|
||||||
<td style="text-align:right;"><?= number_format($journal_euer['privat_entnahmen'], 2, ',', '.') ?> €</td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<h4>Ergebnis</h4>
|
|
||||||
<table class="list">
|
|
||||||
<tbody>
|
|
||||||
<tr class="euer-result <?= $journal_euer['gewinn'] < 0 ? 'negative' : '' ?>">
|
|
||||||
<td><strong>Gewinn / Verlust</strong></td>
|
|
||||||
<td style="text-align:right;"><strong><?= number_format($journal_euer['gewinn'], 2, ',', '.') ?> €</strong></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div style="margin-top:8px;">
|
|
||||||
<a href="<?= url_for('euer.php?year=' . $year . '&csv_journal=1') ?>" class="button-secondary">CSV Export</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php else: ?>
|
|
||||||
<section class="euer-section">
|
|
||||||
<p class="euer-desc">Kein Journal für <?= $year ?> vorhanden.</p>
|
|
||||||
<?php if ($has_missing): ?>
|
|
||||||
<p>Klicke oben auf "Fehlende Buchungen nachholen" um das Journal automatisch zu erstellen.</p>
|
|
||||||
<?php else: ?>
|
|
||||||
<p><a href="<?= url_for('settings.php?tab=journal') ?>">Jahr im Journal anlegen</a></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
|
||||||
</main>
|
|
||||||
<script src="assets/command-palette.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Redirect to new unified EÜR page
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
|
|
||||||
// Extract year from old date params for backwards compatibility
|
|
||||||
$from = $_GET['from'] ?? date('Y-01-01');
|
|
||||||
$year = date('Y', strtotime($from));
|
|
||||||
|
|
||||||
header('Location: ' . url_for('euer.php?year=' . $year));
|
|
||||||
exit;
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/expense_functions.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
$id = (int)($_GET['id'] ?? 0);
|
|
||||||
$expense = get_expense($id);
|
|
||||||
|
|
||||||
if (!$expense || empty($expense['attachment_path'])) {
|
|
||||||
http_response_code(404);
|
|
||||||
echo 'Datei nicht gefunden.';
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$fsPath = __DIR__ . '/' . $expense['attachment_path'];
|
|
||||||
|
|
||||||
if (!is_readable($fsPath)) {
|
|
||||||
http_response_code(404);
|
|
||||||
echo 'Datei nicht gefunden.';
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
header('Content-Type: application/pdf');
|
|
||||||
header('Content-Disposition: inline; filename="Ausgabe-' . $id . '.pdf"');
|
|
||||||
header('Content-Length: ' . filesize($fsPath));
|
|
||||||
readfile($fsPath);
|
|
||||||
exit;
|
|
||||||
|
|
||||||
@@ -1,389 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_once __DIR__ . '/../src/expense_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/invoice_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/journal_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/icons.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
$pdo = get_db();
|
|
||||||
$settings = get_settings();
|
|
||||||
$vat_mode = $settings['vat_mode'] ?? 'klein';
|
|
||||||
$msg = '';
|
|
||||||
$error = '';
|
|
||||||
|
|
||||||
$action = $_GET['action'] ?? '';
|
|
||||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
|
||||||
|
|
||||||
// Aufwandskategorien für Dropdown laden
|
|
||||||
$expense_categories = get_journal_expense_categories(true);
|
|
||||||
|
|
||||||
// Ausgabe als bezahlt markieren (eigene Aktion)
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['mark_paid_id'])) {
|
|
||||||
$pay_id = (int)$_POST['mark_paid_id'];
|
|
||||||
$payment_date = $_POST['payment_date'] ?? date('Y-m-d');
|
|
||||||
|
|
||||||
$pdo->beginTransaction();
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare('UPDATE expenses SET paid = TRUE, payment_date = :pd WHERE id = :id');
|
|
||||||
$stmt->execute([':id' => $pay_id, ':pd' => $payment_date]);
|
|
||||||
|
|
||||||
// Journal-Eintrag erstellen
|
|
||||||
$existing = get_journal_entry_for_expense($pay_id);
|
|
||||||
if (!$existing) {
|
|
||||||
$entry_id = create_journal_entry_from_expense($pay_id);
|
|
||||||
$msg = 'Ausgabe als bezahlt markiert. Journalbuchung #' . $entry_id . ' erstellt.';
|
|
||||||
} else {
|
|
||||||
$msg = 'Ausgabe als bezahlt markiert (Journalbuchung existierte bereits).';
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo->commit();
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$pdo->rollBack();
|
|
||||||
$error = 'Fehler: ' . $e->getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normale Ausgabe speichern (neu oder Update)
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['expense_date']) && !isset($_POST['mark_paid_id'])) {
|
|
||||||
$idPost = isset($_POST['id']) && $_POST['id'] !== '' ? (int)$_POST['id'] : null;
|
|
||||||
$data = [
|
|
||||||
'expense_date' => $_POST['expense_date'] ?? '',
|
|
||||||
'description' => $_POST['description'] ?? '',
|
|
||||||
'category' => $_POST['category'] ?? '',
|
|
||||||
'amount' => (float)($_POST['amount'] ?? 0),
|
|
||||||
'vat_rate' => (float)($_POST['vat_rate'] ?? 0),
|
|
||||||
'expense_category_id' => !empty($_POST['expense_category_id']) ? (int)$_POST['expense_category_id'] : null,
|
|
||||||
'paid' => !empty($_POST['paid']) ? 1 : 0,
|
|
||||||
'payment_date' => $_POST['payment_date'] ?? '',
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!$data['expense_date'] || !$data['description'] || $data['amount'] <= 0) {
|
|
||||||
$error = 'Datum, Beschreibung und Betrag sind Pflichtfelder.';
|
|
||||||
} elseif (!empty($data['paid']) && empty($data['expense_category_id'])) {
|
|
||||||
$error = 'Bei bezahlten Ausgaben ist eine Aufwandskategorie für die Journalbuchung Pflicht.';
|
|
||||||
} else {
|
|
||||||
$pdo->beginTransaction();
|
|
||||||
try {
|
|
||||||
// War vorher schon bezahlt? (für Update-Check)
|
|
||||||
$was_paid = false;
|
|
||||||
$had_journal = false;
|
|
||||||
if ($idPost) {
|
|
||||||
$old_exp = get_expense($idPost);
|
|
||||||
$was_paid = $old_exp && $old_exp['paid'];
|
|
||||||
$had_journal = $old_exp ? (bool)get_journal_entry_for_expense($idPost) : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ausgabe speichern
|
|
||||||
$expense_id = save_expense($idPost, $data);
|
|
||||||
|
|
||||||
// Datei-Upload (PDF) verarbeiten
|
|
||||||
if (!empty($_FILES['attachment']['tmp_name'])) {
|
|
||||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
|
||||||
$mime = finfo_file($finfo, $_FILES['attachment']['tmp_name']);
|
|
||||||
finfo_close($finfo);
|
|
||||||
|
|
||||||
if ($mime === 'application/pdf') {
|
|
||||||
$uploadDir = __DIR__ . '/uploads/expenses';
|
|
||||||
if (!is_dir($uploadDir)) {
|
|
||||||
mkdir($uploadDir, 0775, true);
|
|
||||||
}
|
|
||||||
$targetFile = $uploadDir . '/expense_' . $expense_id . '.pdf';
|
|
||||||
if (move_uploaded_file($_FILES['attachment']['tmp_name'], $targetFile)) {
|
|
||||||
$relPath = 'uploads/expenses/expense_' . $expense_id . '.pdf';
|
|
||||||
$stmt = $pdo->prepare("UPDATE expenses SET attachment_path = :p WHERE id = :id");
|
|
||||||
$stmt->execute([':p' => $relPath, ':id' => $expense_id]);
|
|
||||||
} else {
|
|
||||||
$error = 'Ausgabe gespeichert, aber Datei konnte nicht verschoben werden.';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$error = 'Bitte nur PDF-Dateien als Beleg hochladen.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Journal-Integration
|
|
||||||
if (!empty($data['paid']) && !$had_journal) {
|
|
||||||
// Neu bezahlt → Journal-Eintrag erstellen
|
|
||||||
$existing = get_journal_entry_for_expense($expense_id);
|
|
||||||
if (!$existing) {
|
|
||||||
$entry_id = create_journal_entry_from_expense($expense_id);
|
|
||||||
$msg = 'Ausgabe gespeichert. Journalbuchung #' . $entry_id . ' erstellt.';
|
|
||||||
} else {
|
|
||||||
$msg = 'Ausgabe gespeichert.';
|
|
||||||
}
|
|
||||||
} elseif (empty($data['paid']) && $had_journal) {
|
|
||||||
// War bezahlt, jetzt offen → Journal-Eintrag entfernen
|
|
||||||
delete_journal_entry_for_expense($expense_id);
|
|
||||||
$msg = 'Ausgabe gespeichert. Journalbuchung entfernt (Status: offen).';
|
|
||||||
} else {
|
|
||||||
$msg = $msg ?: 'Ausgabe gespeichert.';
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo->commit();
|
|
||||||
$action = '';
|
|
||||||
$id = null;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$pdo->rollBack();
|
|
||||||
$error = 'Fehler: ' . $e->getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($action === 'delete' && $id) {
|
|
||||||
$exp = get_expense($id);
|
|
||||||
if ($exp) {
|
|
||||||
// Verknüpften Journal-Eintrag löschen
|
|
||||||
delete_journal_entry_for_expense($id);
|
|
||||||
|
|
||||||
if (!empty($exp['attachment_path'])) {
|
|
||||||
$fsPath = __DIR__ . '/' . $exp['attachment_path'];
|
|
||||||
if (is_file($fsPath)) {
|
|
||||||
@unlink($fsPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete_expense($id);
|
|
||||||
$msg = 'Ausgabe und zugehörige Journalbuchung gelöscht.';
|
|
||||||
}
|
|
||||||
$action = '';
|
|
||||||
$id = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$editExpense = null;
|
|
||||||
if ($action === 'edit' && $id) {
|
|
||||||
$editExpense = get_expense($id);
|
|
||||||
}
|
|
||||||
|
|
||||||
$filters = [
|
|
||||||
'from' => $_GET['from'] ?? '',
|
|
||||||
'to' => $_GET['to'] ?? '',
|
|
||||||
'paid' => $_GET['paid'] ?? '',
|
|
||||||
'search' => $_GET['search'] ?? '',
|
|
||||||
];
|
|
||||||
|
|
||||||
$expenses = get_expenses($filters);
|
|
||||||
|
|
||||||
// Journal-Verknüpfungen laden
|
|
||||||
$journal_linked_expenses = [];
|
|
||||||
try {
|
|
||||||
$stmt_jl = $pdo->query("SELECT expense_id, id FROM journal_entries WHERE expense_id IS NOT NULL");
|
|
||||||
foreach ($stmt_jl->fetchAll(PDO::FETCH_ASSOC) as $jl) {
|
|
||||||
$journal_linked_expenses[(int)$jl['expense_id']] = (int)$jl['id'];
|
|
||||||
}
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
// Spalte existiert noch nicht
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Ausgaben</title>
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
<script>
|
|
||||||
function updateVatCalc() {
|
|
||||||
const amount = parseFloat(document.getElementById('expense-amount').value) || 0;
|
|
||||||
const vatRate = parseFloat(document.getElementById('expense-vat-rate').value) || 0;
|
|
||||||
const infoEl = document.getElementById('vat-info');
|
|
||||||
if (vatRate > 0 && amount > 0) {
|
|
||||||
const net = amount / (1 + vatRate / 100);
|
|
||||||
const vat = amount - net;
|
|
||||||
infoEl.textContent = 'Netto: ' + net.toFixed(2).replace('.', ',') + ' € | VorSt: ' + vat.toFixed(2).replace('.', ',') + ' €';
|
|
||||||
infoEl.style.display = 'block';
|
|
||||||
} else {
|
|
||||||
infoEl.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>PIRP</h1>
|
|
||||||
<nav>
|
|
||||||
<a href="<?= url_for('index.php') ?>"><?= icon_dashboard() ?>Dashboard</a>
|
|
||||||
<a href="<?= url_for('invoices.php') ?>"><?= icon_invoices() ?>Rechnungen</a>
|
|
||||||
<a href="<?= url_for('customers.php') ?>"><?= icon_customers() ?>Kunden</a>
|
|
||||||
<a href="<?= url_for('expenses.php') ?>" class="active"><?= icon_expenses() ?>Ausgaben</a>
|
|
||||||
<a href="<?= url_for('belegarchiv.php') ?>"><?= icon_archive() ?>Belege</a>
|
|
||||||
<a href="<?= url_for('journal.php') ?>"><?= icon_journal() ?>Journal</a>
|
|
||||||
<a href="<?= url_for('euer.php') ?>"><?= icon_euer() ?>EÜR</a>
|
|
||||||
<a href="<?= url_for('settings.php') ?>"><?= icon_settings() ?>Einstellungen</a>
|
|
||||||
<a href="<?= url_for('logout.php') ?>"><?= icon_logout() ?>Logout (<?= htmlspecialchars($_SESSION['username'] ?? '') ?>)</a>
|
|
||||||
<span class="cmd-k-hint" onclick="document.dispatchEvent(new KeyboardEvent('keydown',{key:'k',ctrlKey:true}))"><kbd>Ctrl+K</kbd></span>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<?php if ($msg): ?><p class="success"><?= htmlspecialchars($msg) ?></p><?php endif; ?>
|
|
||||||
<?php if ($error): ?><p class="error"><?= htmlspecialchars($error) ?></p><?php endif; ?>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2><?= $editExpense ? 'Ausgabe bearbeiten' : 'Neue Ausgabe' ?></h2>
|
|
||||||
<form method="post" enctype="multipart/form-data">
|
|
||||||
<input type="hidden" name="id" value="<?= htmlspecialchars($editExpense['id'] ?? '') ?>">
|
|
||||||
<div class="flex-row">
|
|
||||||
<label>Datum:
|
|
||||||
<input type="date" name="expense_date" value="<?= htmlspecialchars($editExpense['expense_date'] ?? date('Y-m-d')) ?>" required>
|
|
||||||
</label>
|
|
||||||
<label>Betrag (brutto):
|
|
||||||
<input type="number" step="0.01" name="amount" id="expense-amount"
|
|
||||||
value="<?= htmlspecialchars($editExpense['amount'] ?? '0.00') ?>"
|
|
||||||
required oninput="updateVatCalc()">
|
|
||||||
</label>
|
|
||||||
<?php if ($vat_mode === 'normal'): ?>
|
|
||||||
<label>MwSt-Satz:
|
|
||||||
<select name="vat_rate" id="expense-vat-rate" onchange="updateVatCalc()">
|
|
||||||
<option value="0" <?= (isset($editExpense['vat_rate']) && (float)$editExpense['vat_rate'] == 0) ? 'selected' : '' ?>>0% (keine MwSt)</option>
|
|
||||||
<option value="7" <?= (isset($editExpense['vat_rate']) && (float)$editExpense['vat_rate'] == 7) ? 'selected' : '' ?>>7%</option>
|
|
||||||
<option value="19" <?= (isset($editExpense['vat_rate']) && (float)$editExpense['vat_rate'] == 19) ? 'selected' : (!isset($editExpense) ? 'selected' : '') ?>>19%</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<?php else: ?>
|
|
||||||
<input type="hidden" name="vat_rate" value="0">
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<div id="vat-info" style="display:none; font-size:11px; color:var(--text-dim); margin:-8px 0 8px;"></div>
|
|
||||||
|
|
||||||
<label>Beschreibung:
|
|
||||||
<input type="text" name="description" value="<?= htmlspecialchars($editExpense['description'] ?? '') ?>" required>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="flex-row">
|
|
||||||
<label>Aufwandskategorie:
|
|
||||||
<select name="expense_category_id">
|
|
||||||
<option value="">-- keine --</option>
|
|
||||||
<?php foreach ($expense_categories as $cat): ?>
|
|
||||||
<option value="<?= $cat['id'] ?>" <?= (isset($editExpense['expense_category_id']) && (int)$editExpense['expense_category_id'] === (int)$cat['id']) ? 'selected' : '' ?>>
|
|
||||||
<?= htmlspecialchars($cat['name']) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>Notiz / Kategorie:
|
|
||||||
<input type="text" name="category" value="<?= htmlspecialchars($editExpense['category'] ?? '') ?>" placeholder="Freitext-Notiz">
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-row">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" name="paid" value="1" <?= (isset($editExpense['paid']) ? $editExpense['paid'] : 1) ? 'checked' : '' ?>>
|
|
||||||
bezahlt
|
|
||||||
</label>
|
|
||||||
<label>Zahlungsdatum:
|
|
||||||
<input type="date" name="payment_date" value="<?= htmlspecialchars($editExpense['payment_date'] ?? date('Y-m-d')) ?>">
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label>Beleg (PDF):
|
|
||||||
<input type="file" name="attachment" accept="application/pdf">
|
|
||||||
<?php if (!empty($editExpense['attachment_path'])): ?>
|
|
||||||
<br>Aktueller Beleg:
|
|
||||||
<a href="<?= url_for('expense_file.php?id=' . $editExpense['id']) ?>" target="_blank">PDF öffnen</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button type="submit">Speichern</button>
|
|
||||||
<?php if ($editExpense): ?>
|
|
||||||
<a href="<?= url_for('expenses.php') ?>">Abbrechen</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>Übersicht Ausgaben</h2>
|
|
||||||
<form method="get" class="filters">
|
|
||||||
<label>Von:
|
|
||||||
<input type="date" name="from" value="<?= htmlspecialchars($filters['from']) ?>">
|
|
||||||
</label>
|
|
||||||
<label>Bis:
|
|
||||||
<input type="date" name="to" value="<?= htmlspecialchars($filters['to']) ?>">
|
|
||||||
</label>
|
|
||||||
<label>Status:
|
|
||||||
<select name="paid">
|
|
||||||
<option value="">alle</option>
|
|
||||||
<option value="1" <?= $filters['paid']==='1' ? 'selected' : '' ?>>bezahlt</option>
|
|
||||||
<option value="0" <?= $filters['paid']==='0' ? 'selected' : '' ?>>offen</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>Suche:
|
|
||||||
<input type="text" name="search" value="<?= htmlspecialchars($filters['search']) ?>">
|
|
||||||
</label>
|
|
||||||
<button type="submit">Filtern</button>
|
|
||||||
<a href="<?= url_for('expenses.php') ?>">Zurücksetzen</a>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<table class="list">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>Beschreibung</th>
|
|
||||||
<th>Kategorie</th>
|
|
||||||
<th>Betrag</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Beleg</th>
|
|
||||||
<th>Aktion</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php $total = 0.0; ?>
|
|
||||||
<?php foreach ($expenses as $e): ?>
|
|
||||||
<?php $total += $e['amount']; ?>
|
|
||||||
<tr>
|
|
||||||
<td><?= htmlspecialchars(date('d.m.Y', strtotime($e['expense_date']))) ?></td>
|
|
||||||
<td><?= htmlspecialchars($e['description']) ?></td>
|
|
||||||
<td><?= htmlspecialchars($e['category']) ?></td>
|
|
||||||
<td style="text-align:right;"><?= number_format($e['amount'], 2, ',', '.') ?> €</td>
|
|
||||||
<td><?= $e['paid'] ? 'bezahlt' : 'offen' ?></td>
|
|
||||||
<td>
|
|
||||||
<?php if (!empty($e['attachment_path'])): ?>
|
|
||||||
<a href="<?= url_for('expense_file.php?id=' . $e['id']) ?>" target="_blank">PDF</a>
|
|
||||||
<?php else: ?>
|
|
||||||
-
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="<?= url_for('expenses.php?action=edit&id=' . $e['id']) ?>">Bearbeiten</a>
|
|
||||||
| <a href="<?= url_for('expenses.php?action=delete&id=' . $e['id']) ?>" onclick="return confirm('Ausgabe wirklich löschen?');">Löschen</a>
|
|
||||||
<?php if (!$e['paid']): ?>
|
|
||||||
| <a href="#" onclick="document.getElementById('pay-exp-<?= $e['id'] ?>').style.display='table-row'; return false;">als bezahlt</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (isset($journal_linked_expenses[$e['id']])): ?>
|
|
||||||
| <a href="<?= url_for('journal_entry.php?id=' . $journal_linked_expenses[$e['id']]) ?>">Journal</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php if (!$e['paid']): ?>
|
|
||||||
<tr id="pay-exp-<?= $e['id'] ?>" style="display:none;" class="payment-form-row">
|
|
||||||
<td colspan="7">
|
|
||||||
<form method="post" style="display:inline-flex; gap:8px; align-items:center; padding:4px 0;">
|
|
||||||
<input type="hidden" name="mark_paid_id" value="<?= $e['id'] ?>">
|
|
||||||
<label style="margin:0;">Zahlungsdatum:
|
|
||||||
<input type="date" name="payment_date" value="<?= date('Y-m-d') ?>" required>
|
|
||||||
</label>
|
|
||||||
<button type="submit" onclick="return confirm('Ausgabe als bezahlt markieren und Journal buchen?');">Bezahlt + Journal buchen</button>
|
|
||||||
<a href="#" onclick="this.closest('tr').style.display='none'; return false;">Abbrechen</a>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php if (empty($expenses)): ?>
|
|
||||||
<tr><td colspan="7">Keine Ausgaben gefunden.</td></tr>
|
|
||||||
<?php else: ?>
|
|
||||||
<tr>
|
|
||||||
<td colspan="3"><strong>Summe</strong></td>
|
|
||||||
<td style="text-align:right;"><strong><?= number_format($total, 2, ',', '.') ?> €</strong></td>
|
|
||||||
<td colspan="3"></td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<script>
|
|
||||||
// MwSt-Info beim Laden aktualisieren (bei Edit)
|
|
||||||
document.addEventListener('DOMContentLoaded', updateVatCalc);
|
|
||||||
</script>
|
|
||||||
<script src="assets/command-palette.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,422 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_once __DIR__ . '/../src/recurring_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/pdf_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/journal_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/icons.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
$pdo = get_db();
|
|
||||||
$year = date('Y');
|
|
||||||
$month = (int)date('m');
|
|
||||||
$prev_month = $month === 1 ? 12 : $month - 1;
|
|
||||||
$prev_month_year = $month === 1 ? (int)$year - 1 : (int)$year;
|
|
||||||
|
|
||||||
// === RECHNUNGEN/AUSGABEN ===
|
|
||||||
// Offene Rechnungen
|
|
||||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM invoices WHERE paid = FALSE");
|
|
||||||
$stmt->execute();
|
|
||||||
$open_invoices_count = (int)$stmt->fetchColumn();
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare("SELECT COALESCE(SUM(total_gross),0) FROM invoices WHERE paid = FALSE");
|
|
||||||
$stmt->execute();
|
|
||||||
$open_invoices_sum = (float)$stmt->fetchColumn();
|
|
||||||
|
|
||||||
// Überfällige Rechnungen (> 14 Tage offen)
|
|
||||||
$stmt = $pdo->prepare("SELECT i.*, c.name AS customer_name FROM invoices i JOIN customers c ON c.id = i.customer_id WHERE i.paid = FALSE AND i.invoice_date < NOW() - INTERVAL '14 days' ORDER BY i.invoice_date ASC LIMIT 5");
|
|
||||||
$stmt->execute();
|
|
||||||
$overdue_invoices = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
// Fällige Abo-Rechnungen
|
|
||||||
$pending_recurring = count_pending_recurring_invoices();
|
|
||||||
|
|
||||||
// Konsistenz-Check: Fehlende Buchungen
|
|
||||||
$consistency = check_journal_consistency();
|
|
||||||
$has_unbooked = ($consistency['unbooked_invoices'] > 0 || $consistency['unbooked_expenses'] > 0);
|
|
||||||
|
|
||||||
// GoBD PDF-Status
|
|
||||||
$gobd_status = check_pdf_integrity_status();
|
|
||||||
$gobd_has_problems = ($gobd_status['unarchived'] > 0 || $gobd_status['invalid'] > 0 || $gobd_status['missing_files'] > 0);
|
|
||||||
|
|
||||||
// Letzte 5 Rechnungen
|
|
||||||
$stmt = $pdo->prepare("SELECT i.*, c.name AS customer_name FROM invoices i JOIN customers c ON c.id = i.customer_id ORDER BY i.created_at DESC LIMIT 5");
|
|
||||||
$stmt->execute();
|
|
||||||
$recent_invoices = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
// === JOURNAL MODUL ===
|
|
||||||
$journal_year = get_journal_year_by_year((int)$year);
|
|
||||||
$journal_year_id = $journal_year ? (int)$journal_year['id'] : null;
|
|
||||||
|
|
||||||
$journal_erloese_monat = 0;
|
|
||||||
$journal_wareneingang_monat = 0;
|
|
||||||
$journal_gewinn_monat = 0;
|
|
||||||
$journal_erloese_jahr = 0;
|
|
||||||
$journal_wareneingang_jahr = 0;
|
|
||||||
$journal_gewinn_jahr = 0;
|
|
||||||
$journal_entries_month = 0;
|
|
||||||
$journal_gewinn_prev = 0;
|
|
||||||
|
|
||||||
if ($journal_year_id) {
|
|
||||||
$month_profit = calculate_yearly_profitability($journal_year_id);
|
|
||||||
if (isset($month_profit[$month])) {
|
|
||||||
$journal_erloese_monat = $month_profit[$month]['erloese'];
|
|
||||||
$journal_wareneingang_monat = $month_profit[$month]['wareneingang'];
|
|
||||||
$journal_gewinn_monat = $month_profit[$month]['gewinn'];
|
|
||||||
}
|
|
||||||
if (isset($month_profit[$prev_month])) {
|
|
||||||
$journal_gewinn_prev = $month_profit[$prev_month]['gewinn'];
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($month_profit as $m => $p) {
|
|
||||||
$journal_erloese_jahr += $p['erloese'];
|
|
||||||
$journal_wareneingang_jahr += $p['wareneingang'];
|
|
||||||
$journal_gewinn_jahr += $p['gewinn'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM journal_entries WHERE year_id = :y AND month = :m");
|
|
||||||
$stmt->execute([':y' => $journal_year_id, ':m' => $month]);
|
|
||||||
$journal_entries_month = (int)$stmt->fetchColumn();
|
|
||||||
|
|
||||||
$mt = calculate_monthly_totals($journal_year_id, $month);
|
|
||||||
$journal_kasse_balance = ($mt['kasse_s'] ?? 0) - ($mt['kasse_h'] ?? 0);
|
|
||||||
$journal_bank_balance = ($mt['bank_s'] ?? 0) - ($mt['bank_h'] ?? 0);
|
|
||||||
} else {
|
|
||||||
$journal_kasse_balance = 0;
|
|
||||||
$journal_bank_balance = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sparkline-Daten
|
|
||||||
$sparkline_erloese = [];
|
|
||||||
$sparkline_gewinn = [];
|
|
||||||
$sparkline_wareneingang = [];
|
|
||||||
if ($journal_year_id && !empty($month_profit)) {
|
|
||||||
for ($m = 1; $m <= $month; $m++) {
|
|
||||||
$sparkline_erloese[] = $month_profit[$m]['erloese'] ?? 0;
|
|
||||||
$sparkline_gewinn[] = $month_profit[$m]['gewinn'] ?? 0;
|
|
||||||
$sparkline_wareneingang[] = $month_profit[$m]['wareneingang'] ?? 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Letzte 5 Journal-Einträge
|
|
||||||
$recent_journal = get_recent_journal_entries(5);
|
|
||||||
|
|
||||||
$month_names_full = [
|
|
||||||
1 => 'Januar', 2 => 'Februar', 3 => 'März', 4 => 'April',
|
|
||||||
5 => 'Mai', 6 => 'Juni', 7 => 'Juli', 8 => 'August',
|
|
||||||
9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Dezember',
|
|
||||||
];
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>PIRP Dashboard</title>
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>PIRP</h1>
|
|
||||||
<nav>
|
|
||||||
<a href="<?= url_for('index.php') ?>" class="active"><?= icon_dashboard() ?>Dashboard</a>
|
|
||||||
<a href="<?= url_for('invoices.php') ?>"><?= icon_invoices() ?>Rechnungen</a>
|
|
||||||
<a href="<?= url_for('customers.php') ?>"><?= icon_customers() ?>Kunden</a>
|
|
||||||
<a href="<?= url_for('expenses.php') ?>"><?= icon_expenses() ?>Ausgaben</a>
|
|
||||||
<a href="<?= url_for('belegarchiv.php') ?>"><?= icon_archive() ?>Belege</a>
|
|
||||||
<a href="<?= url_for('journal.php') ?>"><?= icon_journal() ?>Journal</a>
|
|
||||||
<a href="<?= url_for('euer.php') ?>"><?= icon_euer() ?>EÜR</a>
|
|
||||||
<a href="<?= url_for('settings.php') ?>"><?= icon_settings() ?>Einstellungen</a>
|
|
||||||
<a href="<?= url_for('logout.php') ?>"><?= icon_logout() ?>Logout (<?= htmlspecialchars($_SESSION['username'] ?? '') ?>)</a>
|
|
||||||
<span class="cmd-k-hint" onclick="document.dispatchEvent(new KeyboardEvent('keydown',{key:'k',ctrlKey:true}))"><kbd>Ctrl+K</kbd></span>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<?php if ($has_unbooked): ?>
|
|
||||||
<div class="gobd-warning">
|
|
||||||
<strong>Fehlende Journalbuchungen:</strong>
|
|
||||||
<?php if ($consistency['unbooked_invoices'] > 0): ?>
|
|
||||||
<?= $consistency['unbooked_invoices'] ?> bezahlte Rechnung(en)
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($consistency['unbooked_invoices'] > 0 && $consistency['unbooked_expenses'] > 0): ?> und <?php endif; ?>
|
|
||||||
<?php if ($consistency['unbooked_expenses'] > 0): ?>
|
|
||||||
<?= $consistency['unbooked_expenses'] ?> bezahlte Ausgabe(n)
|
|
||||||
<?php endif; ?>
|
|
||||||
ohne Journaleintrag.
|
|
||||||
<a href="<?= url_for('euer.php?year=' . $year . '&fix_missing=1') ?>">Jetzt nachholen</a>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<!-- Finanz-Übersicht -->
|
|
||||||
<h2><?= htmlspecialchars($year) ?> · <?= $month_names_full[$month] ?></h2>
|
|
||||||
|
|
||||||
<?php if ($journal_year_id): ?>
|
|
||||||
|
|
||||||
<!-- Rechnungen / Allgemein -->
|
|
||||||
<div class="dashboard-grid" style="margin-bottom:6px;">
|
|
||||||
<div class="card">
|
|
||||||
<h3>Offen</h3>
|
|
||||||
<p class="big"><?= number_format($open_invoices_sum, 2, ',', '.') ?> €</p>
|
|
||||||
<span style="font-size:10px;color:var(--text-dim);"><?= $open_invoices_count ?> Rechnungen</span>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h3>Abo-Rechnungen</h3>
|
|
||||||
<p class="big"><?= $pending_recurring ?> fällig</p>
|
|
||||||
<?php if ($pending_recurring > 0): ?>
|
|
||||||
<a href="<?= url_for('recurring_generate.php') ?>">Generieren</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h3>Erlöse (Jahr)</h3>
|
|
||||||
<p class="big"><?= number_format($journal_erloese_jahr, 2, ',', '.') ?> €</p>
|
|
||||||
<?php if (count($sparkline_erloese) >= 2): ?>
|
|
||||||
<?= generate_sparkline_svg($sparkline_erloese, '#22c55e') ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
<a href="<?= url_for('journal_summary.php?year_id=' . $journal_year_id) ?>" style="font-size:10px;">Jahresübersicht →</a>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h3>Gewinn (Jahr)</h3>
|
|
||||||
<p class="big" <?= $journal_gewinn_jahr < 0 ? 'style="color:var(--error);"' : '' ?>><?= number_format($journal_gewinn_jahr, 2, ',', '.') ?> €</p>
|
|
||||||
<?php if (count($sparkline_gewinn) >= 2): ?>
|
|
||||||
<?= generate_sparkline_svg($sparkline_gewinn, '#d4882a') ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
<a href="<?= url_for('euer.php?year=' . $year) ?>" style="font-size:10px;">EÜR →</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Journal Monat -->
|
|
||||||
<h2 style="margin-bottom:4px;">
|
|
||||||
Journal · <?= $month_names_full[$month] ?>
|
|
||||||
<a href="<?= url_for('journal.php?year_id=' . $journal_year_id . '&month=' . $month) ?>"
|
|
||||||
style="font-size:10px;font-weight:400;text-transform:none;letter-spacing:0;color:var(--accent);margin-left:10px;">
|
|
||||||
<?= $journal_entries_month ?> Buchungen →
|
|
||||||
</a>
|
|
||||||
</h2>
|
|
||||||
<div class="dashboard-grid" style="margin-bottom:4px;">
|
|
||||||
<div class="card">
|
|
||||||
<h3>Erlöse</h3>
|
|
||||||
<p class="big"><?= number_format($journal_erloese_monat, 2, ',', '.') ?> €</p>
|
|
||||||
<?php if ($journal_gewinn_prev != 0): ?>
|
|
||||||
<?php $gwdiff = $journal_gewinn_monat - $journal_gewinn_prev; ?>
|
|
||||||
<span style="font-size:10px;color:<?= $gwdiff >= 0 ? 'var(--success)' : 'var(--error)' ?>;">
|
|
||||||
<?= $gwdiff >= 0 ? '+' : '' ?><?= number_format($gwdiff, 2, ',', '.') ?> Gewinn gg. Vormonat
|
|
||||||
</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h3>Gewinn</h3>
|
|
||||||
<p class="big" <?= $journal_gewinn_monat < 0 ? 'style="color:var(--error);"' : '' ?>><?= number_format($journal_gewinn_monat, 2, ',', '.') ?> €</p>
|
|
||||||
<?php if (count($sparkline_wareneingang) >= 2): ?>
|
|
||||||
<?= generate_sparkline_svg($sparkline_wareneingang, '#737373') ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h3>Kasse</h3>
|
|
||||||
<p class="big" style="color:<?= $journal_kasse_balance < 0 ? 'var(--error)' : 'var(--accent)' ?>;">
|
|
||||||
<?= number_format($journal_kasse_balance, 2, ',', '.') ?> €
|
|
||||||
</p>
|
|
||||||
<a href="<?= url_for('journal.php?year_id=' . $journal_year_id . '&month=' . $month) ?>" style="font-size:10px;">Journal →</a>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h3>Bank</h3>
|
|
||||||
<p class="big" style="color:<?= $journal_bank_balance < 0 ? 'var(--error)' : 'var(--warning)' ?>;">
|
|
||||||
<?= number_format($journal_bank_balance, 2, ',', '.') ?> €
|
|
||||||
</p>
|
|
||||||
<a href="<?= url_for('journal_entry.php?year_id=' . $journal_year_id . '&month=' . $month) ?>" style="font-size:10px;">+ Neue Buchung</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Monatsvergleich -->
|
|
||||||
<?php if (!empty($month_profit)): ?>
|
|
||||||
<section style="margin-bottom:4px;">
|
|
||||||
<h2>Monatsvergleich <?= $year ?></h2>
|
|
||||||
<div style="padding:8px 0;">
|
|
||||||
<?= generate_monthly_bar_chart_svg($month_profit, $month) ?>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php else: ?>
|
|
||||||
<div class="dashboard-grid">
|
|
||||||
<div class="card">
|
|
||||||
<h3>Offen</h3>
|
|
||||||
<p class="big"><?= number_format($open_invoices_sum, 2, ',', '.') ?> €</p>
|
|
||||||
<span style="font-size:10px;color:var(--text-dim);"><?= $open_invoices_count ?> Rechnungen</span>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h3>Abo-Rechnungen</h3>
|
|
||||||
<p class="big"><?= $pending_recurring ?> fällig</p>
|
|
||||||
<?php if ($pending_recurring > 0): ?>
|
|
||||||
<a href="<?= url_for('recurring_generate.php') ?>">Generieren</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<section>
|
|
||||||
<h2>Journal</h2>
|
|
||||||
<div>
|
|
||||||
<p>Kein Journal für <?= $year ?> angelegt.
|
|
||||||
<a href="<?= url_for('settings.php?tab=journal') ?>">Jahr erstellen</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<!-- Überfällige Rechnungen -->
|
|
||||||
<?php if ($overdue_invoices): ?>
|
|
||||||
<section>
|
|
||||||
<h2>Überfällige Rechnungen</h2>
|
|
||||||
<div>
|
|
||||||
<table class="list">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>Nr.</th>
|
|
||||||
<th>Kunde</th>
|
|
||||||
<th style="text-align:right;">Betrag</th>
|
|
||||||
<th>Tage offen</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($overdue_invoices as $oi): ?>
|
|
||||||
<?php $days = (int)((time() - strtotime($oi['invoice_date'])) / 86400); ?>
|
|
||||||
<tr>
|
|
||||||
<td><?= date('d.m.Y', strtotime($oi['invoice_date'])) ?></td>
|
|
||||||
<td><a href="<?= url_for('invoice_pdf.php?id=' . $oi['id']) ?>" target="_blank"><?= htmlspecialchars($oi['invoice_number']) ?></a></td>
|
|
||||||
<td><?= htmlspecialchars($oi['customer_name']) ?></td>
|
|
||||||
<td style="text-align:right;"><?= number_format((float)$oi['total_gross'], 2, ',', '.') ?> €</td>
|
|
||||||
<td style="color:var(--error);"><strong><?= $days ?> Tage</strong></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<!-- Zwei-Spalten: Letzte Rechnungen + Letzte Buchungen -->
|
|
||||||
<div class="dashboard-two-col">
|
|
||||||
<section>
|
|
||||||
<h2>Letzte Rechnungen</h2>
|
|
||||||
<div>
|
|
||||||
<table class="list">
|
|
||||||
<thead><tr><th>Datum</th><th>Kunde</th><th style="text-align:right;">Betrag</th><th>Status</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($recent_invoices as $ri): ?>
|
|
||||||
<tr>
|
|
||||||
<td><?= date('d.m', strtotime($ri['invoice_date'])) ?></td>
|
|
||||||
<td><?= htmlspecialchars($ri['customer_name']) ?></td>
|
|
||||||
<td style="text-align:right;"><?= number_format((float)$ri['total_gross'], 2, ',', '.') ?></td>
|
|
||||||
<td><?= $ri['paid'] ? 'bezahlt' : 'offen' ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php if (empty($recent_invoices)): ?>
|
|
||||||
<tr><td colspan="4" style="color:var(--text-dim);">Keine Rechnungen</td></tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<a href="<?= url_for('invoices.php') ?>" style="font-size:10px;">Alle Rechnungen</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>Letzte Buchungen</h2>
|
|
||||||
<div>
|
|
||||||
<table class="list">
|
|
||||||
<thead><tr><th>Datum</th><th>Text</th><th style="text-align:right;">Betrag</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($recent_journal as $rj): ?>
|
|
||||||
<tr>
|
|
||||||
<td><?= date('d.m', strtotime($rj['entry_date'])) ?></td>
|
|
||||||
<td>
|
|
||||||
<a href="<?= url_for('journal_entry.php?id=' . $rj['id']) ?>" style="color:inherit;">
|
|
||||||
<?= htmlspecialchars(mb_strimwidth($rj['description'], 0, 40, '...')) ?>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td style="text-align:right;font-family:var(--font-mono);font-size:12px;"><?= number_format((float)$rj['amount'], 2, ',', '.') ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php if (empty($recent_journal)): ?>
|
|
||||||
<tr><td colspan="3" style="color:var(--text-dim);">Keine Buchungen</td></tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:6px;">
|
|
||||||
<a href="<?= url_for('journal.php') ?>" style="font-size:10px;">Alle Buchungen →</a>
|
|
||||||
<?php if ($journal_year_id): ?>
|
|
||||||
<a href="<?= url_for('journal_entry.php?year_id=' . $journal_year_id . '&month=' . $month) ?>" class="button" style="font-size:11px;padding:3px 10px;">+ Neue Buchung</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- GoBD PDF-Status -->
|
|
||||||
<?php if ($gobd_has_problems): ?>
|
|
||||||
<div class="gobd-warning">
|
|
||||||
<h3>GoBD-Warnung: PDF-Archivierung unvollständig</h3>
|
|
||||||
<ul>
|
|
||||||
<?php if ($gobd_status['unarchived'] > 0): ?>
|
|
||||||
<li><strong><?= $gobd_status['unarchived'] ?></strong> Rechnung(en) ohne archivierte PDF</li>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($gobd_status['missing_files'] > 0): ?>
|
|
||||||
<li style="color:var(--error);"><strong><?= $gobd_status['missing_files'] ?></strong> PDF-Datei(en) fehlen auf dem Server</li>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($gobd_status['invalid'] > 0): ?>
|
|
||||||
<li style="color:var(--error);"><strong><?= $gobd_status['invalid'] ?></strong> PDF(s) mit fehlgeschlagener Integritätsprüfung</li>
|
|
||||||
<?php endif; ?>
|
|
||||||
</ul>
|
|
||||||
<?php if (!empty($gobd_status['problems'])): ?>
|
|
||||||
<details>
|
|
||||||
<summary>Betroffene Rechnungen anzeigen</summary>
|
|
||||||
<table class="list">
|
|
||||||
<thead><tr><th>Rechnungsnr.</th><th>Problem</th><th>Aktion</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach (array_slice($gobd_status['problems'], 0, 10) as $prob): ?>
|
|
||||||
<tr>
|
|
||||||
<td><?= htmlspecialchars($prob['invoice_number']) ?></td>
|
|
||||||
<td><?= htmlspecialchars($prob['message']) ?></td>
|
|
||||||
<td><a href="<?= url_for('invoice_pdf.php?id=' . $prob['id']) ?>">PDF neu generieren</a></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php if (count($gobd_status['problems']) > 10): ?>
|
|
||||||
<tr><td colspan="3"><em>... und <?= count($gobd_status['problems']) - 10 ?> weitere</em></td></tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</details>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (!empty($gobd_status['migration_needed'])): ?>
|
|
||||||
<p style="margin-top:4px;margin-bottom:0;">
|
|
||||||
<strong>Datenbank-Migration erforderlich:</strong><br>
|
|
||||||
1. <code>sudo -u postgres psql -d pirp -f /var/www/pirp/tools/migrate_pdf.sql</code><br>
|
|
||||||
2. <code>php /var/www/pirp/tools/run_migration.php</code>
|
|
||||||
</p>
|
|
||||||
<?php elseif ($gobd_status['unarchived'] > 0): ?>
|
|
||||||
<p style="margin-top:4px;margin-bottom:0;">
|
|
||||||
<strong>Migration:</strong> <code>php tools/run_migration.php</code>
|
|
||||||
</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
|
||||||
<div class="gobd-ok">
|
|
||||||
<strong>GoBD OK:</strong>
|
|
||||||
<?= $gobd_status['archived'] ?>/<?= $gobd_status['total_invoices'] ?> archiviert.
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>Schnellzugriff</h2>
|
|
||||||
<div class="flex-row" style="gap:8px;flex-wrap:wrap;">
|
|
||||||
<a href="<?= url_for('invoice_new.php') ?>" class="button">Neue Rechnung</a>
|
|
||||||
<a href="<?= url_for('expenses.php') ?>" class="button secondary">Neue Ausgabe</a>
|
|
||||||
<?php if ($journal_year_id): ?>
|
|
||||||
<a href="<?= url_for('journal_entry.php?year_id=' . $journal_year_id . '&month=' . $month) ?>" class="button secondary">Neue Buchung</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
<a href="<?= url_for('euer.php') ?>" class="button secondary">EÜR</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<script src="assets/command-palette.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/invoice_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/customer_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_once __DIR__ . '/../src/pdf_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/icons.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
$pdo = get_db();
|
|
||||||
$settings = get_settings();
|
|
||||||
$customers = get_customers();
|
|
||||||
$error = '';
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$customer_id = (int)($_POST['customer_id'] ?? 0);
|
|
||||||
$invoice_date = $_POST['invoice_date'] ?: date('Y-m-d');
|
|
||||||
$service_date = $_POST['service_date'] ?: null;
|
|
||||||
$notes_internal = $_POST['notes_internal'] ?? '';
|
|
||||||
|
|
||||||
if ($customer_id <= 0) {
|
|
||||||
$error = 'Bitte einen Kunden auswählen.';
|
|
||||||
} else {
|
|
||||||
$vat_mode = $settings['vat_mode'] ?? 'klein';
|
|
||||||
$vat_rate = (float)($settings['default_vat_rate'] ?? 19.0);
|
|
||||||
|
|
||||||
$items = [];
|
|
||||||
$total_net = 0.0;
|
|
||||||
$count = isset($_POST['item_desc']) ? count($_POST['item_desc']) : 0;
|
|
||||||
|
|
||||||
for ($i = 0; $i < $count; $i++) {
|
|
||||||
$desc = trim($_POST['item_desc'][$i] ?? '');
|
|
||||||
$qty = (float)($_POST['item_qty'][$i] ?? 0);
|
|
||||||
$price= (float)($_POST['item_price'][$i] ?? 0);
|
|
||||||
if ($desc !== '' && $qty > 0 && $price >= 0) {
|
|
||||||
$line_net = $qty * $price;
|
|
||||||
$total_net += $line_net;
|
|
||||||
$items[] = [
|
|
||||||
'position_no' => count($items) + 1,
|
|
||||||
'description' => $desc,
|
|
||||||
'quantity' => $qty,
|
|
||||||
'unit_price' => $price,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($items)) {
|
|
||||||
$error = 'Bitte mindestens eine Position ausfüllen.';
|
|
||||||
} else {
|
|
||||||
if ($vat_mode === 'normal') {
|
|
||||||
$total_vat = round($total_net * $vat_rate / 100, 2);
|
|
||||||
} else {
|
|
||||||
$total_vat = 0.0;
|
|
||||||
}
|
|
||||||
$total_gross = $total_net + $total_vat;
|
|
||||||
|
|
||||||
$pdo->beginTransaction();
|
|
||||||
try {
|
|
||||||
$invoice_number = generate_invoice_number();
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO invoices
|
|
||||||
(invoice_number, customer_id, invoice_date, service_date, vat_mode, vat_rate,
|
|
||||||
payment_terms, notes_internal, total_net, total_vat, total_gross, paid)
|
|
||||||
VALUES (:in, :cid, :idate, :sdate, :vm, :vr, :pt, :ni, :tn, :tv, :tg, FALSE)
|
|
||||||
RETURNING id");
|
|
||||||
$stmt->execute([
|
|
||||||
':in' => $invoice_number,
|
|
||||||
':cid' => $customer_id,
|
|
||||||
':idate'=> $invoice_date,
|
|
||||||
':sdate'=> $service_date,
|
|
||||||
':vm' => $vat_mode,
|
|
||||||
':vr' => $vat_rate,
|
|
||||||
':pt' => $settings['payment_terms'] ?? null,
|
|
||||||
':ni' => $notes_internal,
|
|
||||||
':tn' => $total_net,
|
|
||||||
':tv' => $total_vat,
|
|
||||||
':tg' => $total_gross,
|
|
||||||
]);
|
|
||||||
$invoice_id = $stmt->fetchColumn();
|
|
||||||
|
|
||||||
$stmtItem = $pdo->prepare("INSERT INTO invoice_items
|
|
||||||
(invoice_id, position_no, description, quantity, unit_price, vat_rate)
|
|
||||||
VALUES (:iid, :pn, :d, :q, :up, :vr)");
|
|
||||||
foreach ($items as $it) {
|
|
||||||
$stmtItem->execute([
|
|
||||||
':iid' => $invoice_id,
|
|
||||||
':pn' => $it['position_no'],
|
|
||||||
':d' => $it['description'],
|
|
||||||
':q' => $it['quantity'],
|
|
||||||
':up' => $it['unit_price'],
|
|
||||||
':vr' => $vat_rate,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo->commit();
|
|
||||||
|
|
||||||
// PDF sofort archivieren (GoBD-konform)
|
|
||||||
archive_invoice_pdf($invoice_id);
|
|
||||||
|
|
||||||
header('Location: ' . url_for('invoice_pdf.php?id=' . $invoice_id));
|
|
||||||
exit;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$pdo->rollBack();
|
|
||||||
$error = 'Fehler beim Speichern der Rechnung: ' . $e->getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Neue Rechnung</title>
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
<script>
|
|
||||||
function addRow() {
|
|
||||||
const tbody = document.getElementById('items-body');
|
|
||||||
const index = tbody.children.length;
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
tr.innerHTML = `
|
|
||||||
<td>${index+1}</td>
|
|
||||||
<td><input type="text" name="item_desc[${index}]" size="40"></td>
|
|
||||||
<td><input type="number" step="0.01" name="item_qty[${index}]" value="1"></td>
|
|
||||||
<td><input type="number" step="0.01" name="item_price[${index}]" value="0.00"></td>
|
|
||||||
`;
|
|
||||||
tbody.appendChild(tr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enter springt zum nächsten Feld statt Formular abzuschicken
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const form = document.querySelector('form');
|
|
||||||
|
|
||||||
form.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA' && e.target.type !== 'submit') {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const inputs = Array.from(form.querySelectorAll('input, select, textarea, button[type="submit"]'));
|
|
||||||
const currentIndex = inputs.indexOf(e.target);
|
|
||||||
|
|
||||||
if (currentIndex > -1 && currentIndex < inputs.length - 1) {
|
|
||||||
inputs[currentIndex + 1].focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>PIRP</h1>
|
|
||||||
<nav>
|
|
||||||
<a href="<?= url_for('index.php') ?>"><?= icon_dashboard() ?>Dashboard</a>
|
|
||||||
<a href="<?= url_for('invoices.php') ?>" class="active"><?= icon_invoices() ?>Rechnungen</a>
|
|
||||||
<a href="<?= url_for('customers.php') ?>"><?= icon_customers() ?>Kunden</a>
|
|
||||||
<a href="<?= url_for('expenses.php') ?>"><?= icon_expenses() ?>Ausgaben</a>
|
|
||||||
<a href="<?= url_for('belegarchiv.php') ?>"><?= icon_archive() ?>Belege</a>
|
|
||||||
<a href="<?= url_for('journal.php') ?>"><?= icon_journal() ?>Journal</a>
|
|
||||||
<a href="<?= url_for('euer.php') ?>"><?= icon_euer() ?>EÜR</a>
|
|
||||||
<a href="<?= url_for('settings.php') ?>"><?= icon_settings() ?>Einstellungen</a>
|
|
||||||
<a href="<?= url_for('logout.php') ?>"><?= icon_logout() ?>Logout (<?= htmlspecialchars($_SESSION['username'] ?? '') ?>)</a>
|
|
||||||
<span class="cmd-k-hint" onclick="document.dispatchEvent(new KeyboardEvent('keydown',{key:'k',ctrlKey:true}))"><kbd>Ctrl+K</kbd></span>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<div class="module-subnav">
|
|
||||||
<a href="<?= url_for('invoices.php') ?>">Übersicht</a>
|
|
||||||
<a href="<?= url_for('invoice_new.php') ?>" class="active">Neue Rechnung</a>
|
|
||||||
<a href="<?= url_for('recurring.php') ?>">Abo-Rechnungen</a>
|
|
||||||
</div>
|
|
||||||
<?php if ($error): ?><p class="error"><?= htmlspecialchars($error) ?></p><?php endif; ?>
|
|
||||||
|
|
||||||
<form method="post">
|
|
||||||
<label>Kunde:
|
|
||||||
<select name="customer_id" required>
|
|
||||||
<option value="">-- wählen --</option>
|
|
||||||
<?php foreach ($customers as $c): ?>
|
|
||||||
<option value="<?= $c['id'] ?>"><?= htmlspecialchars($c['name']) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="flex-row">
|
|
||||||
<label>Rechnungsdatum:
|
|
||||||
<input type="date" name="invoice_date" value="<?= htmlspecialchars(date('Y-m-d')) ?>">
|
|
||||||
</label>
|
|
||||||
<label>Leistungsdatum:
|
|
||||||
<input type="date" name="service_date">
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Positionen</h2>
|
|
||||||
<table class="list">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Pos.</th>
|
|
||||||
<th>Beschreibung</th>
|
|
||||||
<th>Menge</th>
|
|
||||||
<th>Einzelpreis (netto)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="items-body">
|
|
||||||
<?php for ($i = 0; $i < 3; $i++): ?>
|
|
||||||
<tr>
|
|
||||||
<td><?= $i+1 ?></td>
|
|
||||||
<td><input type="text" name="item_desc[<?= $i ?>]" size="40"></td>
|
|
||||||
<td><input type="number" step="0.01" name="item_qty[<?= $i ?>]" value="1"></td>
|
|
||||||
<td><input type="number" step="0.01" name="item_price[<?= $i ?>]" value="0.00"></td>
|
|
||||||
</tr>
|
|
||||||
<?php endfor; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<button type="button" onclick="addRow()">Position hinzufügen</button>
|
|
||||||
|
|
||||||
<label>Interne Notizen (nicht auf Rechnung):
|
|
||||||
<textarea name="notes_internal" rows="3"></textarea>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button type="submit">Rechnung speichern & PDF anzeigen</button>
|
|
||||||
</form>
|
|
||||||
</main>
|
|
||||||
<script src="assets/command-palette.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* GoBD-konforme PDF-Auslieferung
|
|
||||||
*
|
|
||||||
* Diese Datei liefert archivierte, unveränderliche Rechnungs-PDFs aus.
|
|
||||||
* Bei erstmaligem Aufruf wird die PDF generiert und permanent gespeichert.
|
|
||||||
*/
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_once __DIR__ . '/../src/pdf_functions.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
$id = (int)($_GET['id'] ?? 0);
|
|
||||||
|
|
||||||
if ($id <= 0) {
|
|
||||||
die('Ungültige Rechnungs-ID.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prüfe ob archivierte PDF existiert
|
|
||||||
$pdfPath = get_archived_pdf_path($id);
|
|
||||||
|
|
||||||
if (!$pdfPath) {
|
|
||||||
// Noch keine archivierte PDF - jetzt erstellen
|
|
||||||
$pdfPath = archive_invoice_pdf($id);
|
|
||||||
|
|
||||||
if (!$pdfPath) {
|
|
||||||
// Diagnose: Warum konnte die PDF nicht erstellt werden?
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->prepare("SELECT i.id, i.invoice_number, i.customer_id, c.id AS cust_exists
|
|
||||||
FROM invoices i
|
|
||||||
LEFT JOIN customers c ON c.id = i.customer_id
|
|
||||||
WHERE i.id = :id");
|
|
||||||
$stmt->execute([':id' => $id]);
|
|
||||||
$diag = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
if (!$diag) {
|
|
||||||
die('Fehler: Rechnung nicht gefunden (ID: ' . $id . ')');
|
|
||||||
}
|
|
||||||
if (!$diag['cust_exists']) {
|
|
||||||
die('Fehler: Kunde zur Rechnung ' . htmlspecialchars($diag['invoice_number']) . ' existiert nicht mehr.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prüfe Verzeichnis-Berechtigungen
|
|
||||||
$uploadBase = __DIR__ . '/uploads/invoices';
|
|
||||||
if (!is_dir($uploadBase)) {
|
|
||||||
die('Fehler: Upload-Verzeichnis fehlt (' . $uploadBase . '). Bitte erstellen mit: mkdir -p ' . $uploadBase);
|
|
||||||
}
|
|
||||||
if (!is_writable($uploadBase)) {
|
|
||||||
die('Fehler: Upload-Verzeichnis nicht beschreibbar (' . $uploadBase . ')');
|
|
||||||
}
|
|
||||||
|
|
||||||
die('Fehler beim Generieren der PDF. Bitte Logs prüfen.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vollständiger Dateipfad
|
|
||||||
$fullPath = __DIR__ . '/' . $pdfPath;
|
|
||||||
|
|
||||||
if (!file_exists($fullPath) || !is_readable($fullPath)) {
|
|
||||||
die('PDF-Datei nicht gefunden.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Integritätsprüfung
|
|
||||||
$isValid = verify_invoice_pdf($id);
|
|
||||||
if ($isValid === false) {
|
|
||||||
// WARNUNG: PDF wurde möglicherweise manipuliert!
|
|
||||||
error_log("WARNUNG: Integritätsprüfung fehlgeschlagen für Rechnung ID $id");
|
|
||||||
// Optional: Warnung anzeigen statt die PDF auszuliefern
|
|
||||||
// die('PDF-Integritätsfehler! Die Datei wurde möglicherweise manipuliert.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rechnungsnummer für Dateinamen holen
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->prepare("SELECT invoice_number FROM invoices WHERE id = :id");
|
|
||||||
$stmt->execute([':id' => $id]);
|
|
||||||
$invoiceNumber = $stmt->fetchColumn() ?: 'Rechnung';
|
|
||||||
|
|
||||||
// PDF ausliefern
|
|
||||||
header('Content-Type: application/pdf');
|
|
||||||
header('Content-Disposition: inline; filename="Rechnung-' . $invoiceNumber . '.pdf"');
|
|
||||||
header('Content-Length: ' . filesize($fullPath));
|
|
||||||
header('Cache-Control: private, max-age=0, must-revalidate');
|
|
||||||
readfile($fullPath);
|
|
||||||
exit;
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_once __DIR__ . '/../src/invoice_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/pdf_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/journal_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/icons.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
|
||||||
if (!$id) {
|
|
||||||
header('Location: ' . url_for('invoices.php'));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$inv = get_invoice_with_customer($id);
|
|
||||||
if (!$inv) {
|
|
||||||
header('Location: ' . url_for('invoices.php?msg=' . urlencode('Rechnung nicht gefunden.')));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bereits storniert?
|
|
||||||
if (!empty($inv['is_storno'])) {
|
|
||||||
header('Location: ' . url_for('invoices.php?msg=' . urlencode('Diese Rechnung ist selbst eine Stornorechnung.')));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prüfe ob schon Storno existiert
|
|
||||||
$pdo = get_db();
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("SELECT id FROM invoices WHERE storno_of = :id LIMIT 1");
|
|
||||||
$stmt->execute([':id' => $id]);
|
|
||||||
$existing_storno = $stmt->fetchColumn();
|
|
||||||
} catch (\PDOException $e) {
|
|
||||||
$existing_storno = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($existing_storno) {
|
|
||||||
header('Location: ' . url_for('invoices.php?msg=' . urlencode('Für diese Rechnung existiert bereits eine Stornorechnung.')));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$error = '';
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
try {
|
|
||||||
$storno_id = create_storno_invoice($id);
|
|
||||||
archive_invoice_pdf($storno_id);
|
|
||||||
|
|
||||||
// Journalbuchung stornieren falls vorhanden
|
|
||||||
$original_entry = get_journal_entry_for_invoice($id);
|
|
||||||
if ($original_entry) {
|
|
||||||
create_storno_journal_entry($id, $storno_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare("SELECT invoice_number FROM invoices WHERE id = :id");
|
|
||||||
$stmt->execute([':id' => $storno_id]);
|
|
||||||
$storno_number = $stmt->fetchColumn();
|
|
||||||
|
|
||||||
header('Location: ' . url_for('invoices.php?msg=' . urlencode('Stornorechnung ' . $storno_number . ' erstellt.')));
|
|
||||||
exit;
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$error = $e->getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Storno – <?= htmlspecialchars($inv['invoice_number']) ?></title>
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>PIRP</h1>
|
|
||||||
<nav>
|
|
||||||
<a href="<?= url_for('index.php') ?>"><?= icon_dashboard() ?>Dashboard</a>
|
|
||||||
<a href="<?= url_for('invoices.php') ?>" class="active"><?= icon_invoices() ?>Rechnungen</a>
|
|
||||||
<a href="<?= url_for('customers.php') ?>"><?= icon_customers() ?>Kunden</a>
|
|
||||||
<a href="<?= url_for('expenses.php') ?>"><?= icon_expenses() ?>Ausgaben</a>
|
|
||||||
<a href="<?= url_for('belegarchiv.php') ?>"><?= icon_archive() ?>Belege</a>
|
|
||||||
<a href="<?= url_for('journal.php') ?>"><?= icon_journal() ?>Journal</a>
|
|
||||||
<a href="<?= url_for('euer.php') ?>"><?= icon_euer() ?>EÜR</a>
|
|
||||||
<a href="<?= url_for('settings.php') ?>"><?= icon_settings() ?>Einstellungen</a>
|
|
||||||
<a href="<?= url_for('logout.php') ?>"><?= icon_logout() ?>Logout (<?= htmlspecialchars($_SESSION['username'] ?? '') ?>)</a>
|
|
||||||
<span class="cmd-k-hint" onclick="document.dispatchEvent(new KeyboardEvent('keydown',{key:'k',ctrlKey:true}))"><kbd>Ctrl+K</kbd></span>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<div class="module-subnav">
|
|
||||||
<a href="<?= url_for('invoices.php') ?>">← Zurück zu Rechnungen</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($error): ?>
|
|
||||||
<p class="error"><?= htmlspecialchars($error) ?></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>Stornorechnung erstellen</h2>
|
|
||||||
<div style="max-width:520px;">
|
|
||||||
<table class="list" style="margin-bottom:16px;">
|
|
||||||
<tr><td style="color:var(--text-muted);width:140px;">Rechnungsnummer</td><td><strong><?= htmlspecialchars($inv['invoice_number']) ?></strong></td></tr>
|
|
||||||
<tr><td style="color:var(--text-muted);">Kunde</td><td><?= htmlspecialchars($inv['customer_name']) ?></td></tr>
|
|
||||||
<tr><td style="color:var(--text-muted);">Datum</td><td><?= date('d.m.Y', strtotime($inv['invoice_date'])) ?></td></tr>
|
|
||||||
<tr><td style="color:var(--text-muted);">Betrag</td><td><?= number_format($inv['total_gross'], 2, ',', '.') ?> €</td></tr>
|
|
||||||
<tr><td style="color:var(--text-muted);">Status</td><td><?= $inv['paid'] ? 'bezahlt' : 'offen' ?></td></tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div class="gobd-warning" style="margin-bottom:16px;">
|
|
||||||
<strong>Achtung:</strong> Es wird eine neue Rechnung mit negativen Beträgen (Stornorechnung) erstellt.
|
|
||||||
<?php if ($inv['paid']): ?>
|
|
||||||
Die bestehende Journalbuchung wird automatisch storniert (Gegenbuchung).
|
|
||||||
<?php else: ?>
|
|
||||||
Die Rechnung ist noch nicht bezahlt – es wird keine Journalbuchung storniert.
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form method="post">
|
|
||||||
<button type="submit" class="button-danger">Stornorechnung erstellen</button>
|
|
||||||
<a href="<?= url_for('invoices.php') ?>" style="margin-left:12px;">Abbrechen</a>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<script src="assets/command-palette.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_once __DIR__ . '/../src/invoice_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/journal_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/icons.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
$pdo = get_db();
|
|
||||||
|
|
||||||
$msg = '';
|
|
||||||
|
|
||||||
// Rechnung als bezahlt markieren + automatische Journalbuchung
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['mark_paid_id'])) {
|
|
||||||
$id = (int)$_POST['mark_paid_id'];
|
|
||||||
$payment_date = $_POST['payment_date'] ?? date('Y-m-d');
|
|
||||||
|
|
||||||
$pdo->beginTransaction();
|
|
||||||
try {
|
|
||||||
// Bezahlt-Status und Zahlungsdatum setzen
|
|
||||||
$stmt = $pdo->prepare('UPDATE invoices SET paid = TRUE, payment_date = :pd WHERE id = :id');
|
|
||||||
$stmt->execute([':id' => $id, ':pd' => $payment_date]);
|
|
||||||
|
|
||||||
// Automatisch Journalbuchung erstellen
|
|
||||||
$existing = get_journal_entry_for_invoice($id);
|
|
||||||
if (!$existing) {
|
|
||||||
$entry_id = create_journal_entry_from_invoice($id);
|
|
||||||
$msg = 'Rechnung als bezahlt markiert. Journalbuchung #' . $entry_id . ' erstellt.';
|
|
||||||
} else {
|
|
||||||
$msg = 'Rechnung als bezahlt markiert (Journalbuchung existierte bereits).';
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo->commit();
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$pdo->rollBack();
|
|
||||||
$msg = 'Fehler: ' . $e->getMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
header('Location: ' . url_for('invoices.php?msg=' . urlencode($msg)));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nachricht aus Redirect anzeigen
|
|
||||||
if (isset($_GET['msg'])) {
|
|
||||||
$msg = $_GET['msg'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$filter_number = trim($_GET['number'] ?? '');
|
|
||||||
$filter_customer = trim($_GET['customer'] ?? '');
|
|
||||||
$filter_from = trim($_GET['from'] ?? '');
|
|
||||||
$filter_to = trim($_GET['to'] ?? '');
|
|
||||||
|
|
||||||
$where = "WHERE 1=1";
|
|
||||||
$params = [];
|
|
||||||
|
|
||||||
if ($filter_number !== '') {
|
|
||||||
$where .= " AND i.invoice_number ILIKE :num";
|
|
||||||
$params[':num'] = '%' . $filter_number . '%';
|
|
||||||
}
|
|
||||||
if ($filter_customer !== '') {
|
|
||||||
$where .= " AND c.name ILIKE :cust";
|
|
||||||
$params[':cust'] = '%' . $filter_customer . '%';
|
|
||||||
}
|
|
||||||
if ($filter_from !== '') {
|
|
||||||
$where .= " AND i.invoice_date >= :from";
|
|
||||||
$params[':from'] = $filter_from;
|
|
||||||
}
|
|
||||||
if ($filter_to !== '') {
|
|
||||||
$where .= " AND i.invoice_date <= :to";
|
|
||||||
$params[':to'] = $filter_to;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sql = "SELECT i.*, c.name AS customer_name, c.customer_number,
|
|
||||||
COALESCE(i.is_storno, FALSE) AS is_storno,
|
|
||||||
i.storno_of,
|
|
||||||
storno_child.invoice_number AS storno_child_number,
|
|
||||||
storno_child.id AS storno_child_id
|
|
||||||
FROM invoices i
|
|
||||||
JOIN customers c ON c.id = i.customer_id
|
|
||||||
LEFT JOIN invoices storno_child ON storno_child.storno_of = i.id
|
|
||||||
$where ORDER BY i.invoice_date DESC, i.id DESC";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
} catch (\PDOException $e) {
|
|
||||||
// is_storno/storno_of noch nicht migriert — Fallback
|
|
||||||
$sql = "SELECT i.*, c.name AS customer_name, c.customer_number,
|
|
||||||
FALSE AS is_storno, NULL AS storno_of,
|
|
||||||
NULL AS storno_child_number, NULL AS storno_child_id
|
|
||||||
FROM invoices i
|
|
||||||
JOIN customers c ON c.id = i.customer_id
|
|
||||||
$where ORDER BY i.invoice_date DESC, i.id DESC";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
}
|
|
||||||
$invoices = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
// Verknüpfte Journal-Einträge laden (nur wenn Spalte existiert)
|
|
||||||
$journal_linked = [];
|
|
||||||
try {
|
|
||||||
$stmt_jl = $pdo->query("SELECT invoice_id, id FROM journal_entries WHERE invoice_id IS NOT NULL");
|
|
||||||
foreach ($stmt_jl->fetchAll(PDO::FETCH_ASSOC) as $jl) {
|
|
||||||
$journal_linked[(int)$jl['invoice_id']] = (int)$jl['id'];
|
|
||||||
}
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
// Spalte invoice_id existiert noch nicht - ignorieren
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mahnungen pro Rechnung laden
|
|
||||||
$mahnungen_count = [];
|
|
||||||
try {
|
|
||||||
$stmt_m = $pdo->query("SELECT invoice_id, COUNT(*) AS cnt, MAX(level) AS max_level FROM mahnungen GROUP BY invoice_id");
|
|
||||||
foreach ($stmt_m->fetchAll(PDO::FETCH_ASSOC) as $m) {
|
|
||||||
$mahnungen_count[(int)$m['invoice_id']] = ['cnt' => $m['cnt'], 'level' => $m['max_level']];
|
|
||||||
}
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
// Tabelle noch nicht migriert
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Rechnungen</title>
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>PIRP</h1>
|
|
||||||
<nav>
|
|
||||||
<a href="<?= url_for('index.php') ?>"><?= icon_dashboard() ?>Dashboard</a>
|
|
||||||
<a href="<?= url_for('invoices.php') ?>" class="active"><?= icon_invoices() ?>Rechnungen</a>
|
|
||||||
<a href="<?= url_for('customers.php') ?>"><?= icon_customers() ?>Kunden</a>
|
|
||||||
<a href="<?= url_for('expenses.php') ?>"><?= icon_expenses() ?>Ausgaben</a>
|
|
||||||
<a href="<?= url_for('belegarchiv.php') ?>"><?= icon_archive() ?>Belege</a>
|
|
||||||
<a href="<?= url_for('journal.php') ?>"><?= icon_journal() ?>Journal</a>
|
|
||||||
<a href="<?= url_for('euer.php') ?>"><?= icon_euer() ?>EÜR</a>
|
|
||||||
<a href="<?= url_for('settings.php') ?>"><?= icon_settings() ?>Einstellungen</a>
|
|
||||||
<a href="<?= url_for('logout.php') ?>"><?= icon_logout() ?>Logout (<?= htmlspecialchars($_SESSION['username'] ?? '') ?>)</a>
|
|
||||||
<span class="cmd-k-hint" onclick="document.dispatchEvent(new KeyboardEvent('keydown',{key:'k',ctrlKey:true}))"><kbd>Ctrl+K</kbd></span>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<div class="module-subnav">
|
|
||||||
<a href="<?= url_for('invoices.php') ?>" class="active">Übersicht</a>
|
|
||||||
<a href="<?= url_for('invoice_new.php') ?>">Neue Rechnung</a>
|
|
||||||
<a href="<?= url_for('recurring.php') ?>">Abo-Rechnungen</a>
|
|
||||||
</div>
|
|
||||||
<?php if ($msg): ?><p class="success"><?= htmlspecialchars($msg) ?></p><?php endif; ?>
|
|
||||||
|
|
||||||
<form method="get" class="filters">
|
|
||||||
<label>Rechnungsnummer:
|
|
||||||
<input type="text" name="number" value="<?= htmlspecialchars($filter_number) ?>">
|
|
||||||
</label>
|
|
||||||
<label>Kunde:
|
|
||||||
<input type="text" name="customer" value="<?= htmlspecialchars($filter_customer) ?>">
|
|
||||||
</label>
|
|
||||||
<label>Von:
|
|
||||||
<input type="date" name="from" value="<?= htmlspecialchars($filter_from) ?>">
|
|
||||||
</label>
|
|
||||||
<label>Bis:
|
|
||||||
<input type="date" name="to" value="<?= htmlspecialchars($filter_to) ?>">
|
|
||||||
</label>
|
|
||||||
<button type="submit">Filtern</button>
|
|
||||||
<a href="<?= url_for('invoices.php') ?>">Zurücksetzen</a>
|
|
||||||
<a href="<?= url_for('invoices_csv.php') . '?number=' . urlencode($filter_number) . '&customer=' . urlencode($filter_customer) . '&from=' . urlencode($filter_from) . '&to=' . urlencode($filter_to) ?>" class="button-secondary">Export CSV</a>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<table class="list">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>Nr.</th>
|
|
||||||
<th>Kunde</th>
|
|
||||||
<th>Betrag (brutto)</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Aktion</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($invoices as $inv): ?>
|
|
||||||
<?php
|
|
||||||
$is_storno = !empty($inv['is_storno']);
|
|
||||||
$has_storno_child = !empty($inv['storno_child_id']);
|
|
||||||
$mahnung_info = $mahnungen_count[(int)$inv['id']] ?? null;
|
|
||||||
?>
|
|
||||||
<tr <?= $is_storno ? 'style="opacity:0.6;"' : '' ?>>
|
|
||||||
<td><?= htmlspecialchars(date('d.m.Y', strtotime($inv['invoice_date']))) ?></td>
|
|
||||||
<td>
|
|
||||||
<?= htmlspecialchars($inv['invoice_number']) ?>
|
|
||||||
<?php if ($is_storno): ?>
|
|
||||||
<span style="font-size:9px;color:var(--error);font-weight:600;margin-left:4px;">STORNO</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($has_storno_child): ?>
|
|
||||||
<span style="font-size:9px;color:var(--text-muted);margin-left:4px;" title="Storniert durch <?= htmlspecialchars($inv['storno_child_number']) ?>">storniert</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<td><?= htmlspecialchars($inv['customer_name']) ?></td>
|
|
||||||
<td style="text-align:right;"><?= number_format($inv['total_gross'], 2, ',', '.') ?> €</td>
|
|
||||||
<td>
|
|
||||||
<?= $inv['paid'] ? 'bezahlt' : 'offen' ?>
|
|
||||||
<?php if ($mahnung_info): ?>
|
|
||||||
<a href="<?= url_for('belegarchiv.php?invoice_id=' . $inv['id'] . '&type=mahnung') ?>"
|
|
||||||
style="font-size:9px;color:var(--warning);margin-left:4px;font-weight:600;">
|
|
||||||
M<?= $mahnung_info['level'] ?>
|
|
||||||
</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="<?= url_for('invoice_pdf.php?id=' . $inv['id']) ?>" target="_blank">PDF</a>
|
|
||||||
<?php if (!$inv['paid']): ?>
|
|
||||||
| <a href="#" onclick="document.getElementById('pay-form-<?= $inv['id'] ?>').style.display='block'; return false;">bezahlt</a>
|
|
||||||
| <a href="<?= url_for('mahnung_new.php?invoice_id=' . $inv['id']) ?>">Mahnung</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($inv['paid'] && isset($journal_linked[$inv['id']])): ?>
|
|
||||||
| <a href="<?= url_for('journal_entry.php?id=' . $journal_linked[$inv['id']]) ?>">Journal</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (!$is_storno && !$has_storno_child): ?>
|
|
||||||
| <a href="<?= url_for('invoice_storno.php?id=' . $inv['id']) ?>" style="color:var(--error);">Storno</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php if (!$inv['paid']): ?>
|
|
||||||
<tr id="pay-form-<?= $inv['id'] ?>" style="display:none;" class="payment-form-row">
|
|
||||||
<td colspan="6">
|
|
||||||
<form method="post" style="display:inline-flex; gap:8px; align-items:center; padding:4px 0;">
|
|
||||||
<input type="hidden" name="mark_paid_id" value="<?= $inv['id'] ?>">
|
|
||||||
<label style="margin:0;">Zahlungsdatum:
|
|
||||||
<input type="date" name="payment_date" value="<?= date('Y-m-d') ?>" required>
|
|
||||||
</label>
|
|
||||||
<button type="submit" onclick="return confirm('Rechnung als bezahlt markieren und Journalbuchung erstellen?');">Bezahlt + Journal buchen</button>
|
|
||||||
<a href="#" onclick="this.closest('tr').style.display='none'; return false;">Abbrechen</a>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php if (empty($invoices)): ?>
|
|
||||||
<tr><td colspan="6">Keine Rechnungen gefunden.</td></tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</main>
|
|
||||||
<script src="assets/command-palette.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
$pdo = get_db();
|
|
||||||
|
|
||||||
$filter_number = trim($_GET['number'] ?? '');
|
|
||||||
$filter_customer = trim($_GET['customer'] ?? '');
|
|
||||||
$filter_from = trim($_GET['from'] ?? '');
|
|
||||||
$filter_to = trim($_GET['to'] ?? '');
|
|
||||||
|
|
||||||
$sql = "SELECT i.*, c.name AS customer_name
|
|
||||||
FROM invoices i
|
|
||||||
JOIN customers c ON c.id = i.customer_id
|
|
||||||
WHERE 1=1";
|
|
||||||
$params = [];
|
|
||||||
|
|
||||||
if ($filter_number !== '') {
|
|
||||||
$sql .= " AND i.invoice_number ILIKE :num";
|
|
||||||
$params[':num'] = '%' . $filter_number . '%';
|
|
||||||
}
|
|
||||||
if ($filter_customer !== '') {
|
|
||||||
$sql .= " AND c.name ILIKE :cust";
|
|
||||||
$params[':cust'] = '%' . $filter_customer . '%';
|
|
||||||
}
|
|
||||||
if ($filter_from !== '') {
|
|
||||||
$sql .= " AND i.invoice_date >= :from";
|
|
||||||
$params[':from'] = $filter_from;
|
|
||||||
}
|
|
||||||
if ($filter_to !== '') {
|
|
||||||
$sql .= " AND i.invoice_date <= :to";
|
|
||||||
$params[':to'] = $filter_to;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql .= " ORDER BY i.invoice_date ASC, i.id ASC";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
header('Content-Type: text/csv; charset=utf-8');
|
|
||||||
header('Content-Disposition: attachment; filename="invoices_export_' . date('Y-m-d') . '.csv"');
|
|
||||||
|
|
||||||
$out = fopen('php://output', 'w');
|
|
||||||
fputcsv($out, ['Datum', 'Rechnungsnummer', 'Kunde', 'Netto', 'USt', 'Brutto', 'Status'], ';');
|
|
||||||
|
|
||||||
foreach ($rows as $r) {
|
|
||||||
fputcsv($out, [
|
|
||||||
date('d.m.Y', strtotime($r['invoice_date'])),
|
|
||||||
$r['invoice_number'],
|
|
||||||
$r['customer_name'],
|
|
||||||
number_format($r['total_net'], 2, ',', ''),
|
|
||||||
number_format($r['total_vat'], 2, ',', ''),
|
|
||||||
number_format($r['total_gross'], 2, ',', ''),
|
|
||||||
$r['paid'] ? 'bezahlt' : 'offen',
|
|
||||||
], ';');
|
|
||||||
}
|
|
||||||
fclose($out);
|
|
||||||
exit;
|
|
||||||
@@ -1,559 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_once __DIR__ . '/../src/journal_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/icons.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
$msg = '';
|
|
||||||
$error = '';
|
|
||||||
|
|
||||||
// ---- Aktionen ----
|
|
||||||
if (isset($_GET['delete'])) {
|
|
||||||
$del_id = (int)$_GET['delete'];
|
|
||||||
delete_journal_entry($del_id);
|
|
||||||
$msg = 'Buchung gelöscht.';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Umsatz-Zusammenfassung speichern
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['form'] ?? '') === 'summary_values') {
|
|
||||||
$sv_year_id = (int)$_POST['year_id'];
|
|
||||||
$sv_month = (int)$_POST['month'];
|
|
||||||
$values = $_POST['summary_val'] ?? [];
|
|
||||||
save_journal_monthly_summary_values($sv_year_id, $sv_month, $values);
|
|
||||||
$msg = 'Umsatzübersicht gespeichert.';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Jahr und Monat bestimmen ----
|
|
||||||
$years = get_journal_years();
|
|
||||||
|
|
||||||
$year_id = isset($_GET['year_id']) ? (int)$_GET['year_id'] : null;
|
|
||||||
$month = isset($_GET['month']) ? (int)$_GET['month'] : (int)date('n');
|
|
||||||
|
|
||||||
if (!$year_id && $years) {
|
|
||||||
$current_cal_year = (int)date('Y');
|
|
||||||
foreach ($years as $y) {
|
|
||||||
if ($y['year'] == $current_cal_year) {
|
|
||||||
$year_id = $y['id'];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!$year_id) $year_id = $years[0]['id'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$current_year = null;
|
|
||||||
foreach ($years as $y) {
|
|
||||||
if ($y['id'] == $year_id) {
|
|
||||||
$current_year = $y;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Daten laden ----
|
|
||||||
$columns = [];
|
|
||||||
$entries = [];
|
|
||||||
$totals = [];
|
|
||||||
$summary_items = [];
|
|
||||||
$summary_values = [];
|
|
||||||
|
|
||||||
if ($year_id) {
|
|
||||||
$columns = get_journal_columns();
|
|
||||||
$entries = get_journal_entries($year_id, $month);
|
|
||||||
$totals = calculate_monthly_totals($year_id, $month);
|
|
||||||
$summary_items = get_journal_summary_items(true);
|
|
||||||
$summary_values_raw = get_journal_monthly_summary_values($year_id, $month);
|
|
||||||
foreach ($summary_values_raw as $sv) {
|
|
||||||
$summary_values[$sv['summary_item_id']] = $sv['amount'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spaltengruppen für colspan
|
|
||||||
$groups = [];
|
|
||||||
foreach ($columns as $col) {
|
|
||||||
$g = $col['group'];
|
|
||||||
if (!isset($groups[$g])) $groups[$g] = 0;
|
|
||||||
$groups[$g]++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Konto-Optionen als JSON
|
|
||||||
$acct_options_json = json_encode(get_journal_account_options());
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Journal <?= $current_year ? (int)$current_year['year'] : '' ?> - <?= journal_month_name_full($month) ?></title>
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>PIRP</h1>
|
|
||||||
<nav>
|
|
||||||
<a href="<?= url_for('index.php') ?>"><?= icon_dashboard() ?>Dashboard</a>
|
|
||||||
<a href="<?= url_for('invoices.php') ?>"><?= icon_invoices() ?>Rechnungen</a>
|
|
||||||
<a href="<?= url_for('customers.php') ?>"><?= icon_customers() ?>Kunden</a>
|
|
||||||
<a href="<?= url_for('expenses.php') ?>"><?= icon_expenses() ?>Ausgaben</a>
|
|
||||||
<a href="<?= url_for('belegarchiv.php') ?>"><?= icon_archive() ?>Belege</a>
|
|
||||||
<a href="<?= url_for('journal.php') ?>" class="active"><?= icon_journal() ?>Journal</a>
|
|
||||||
<a href="<?= url_for('euer.php') ?>"><?= icon_euer() ?>EÜR</a>
|
|
||||||
<a href="<?= url_for('settings.php') ?>"><?= icon_settings() ?>Einstellungen</a>
|
|
||||||
<a href="<?= url_for('logout.php') ?>"><?= icon_logout() ?>Logout (<?= htmlspecialchars($_SESSION['username'] ?? '') ?>)</a>
|
|
||||||
<span class="cmd-k-hint" onclick="document.dispatchEvent(new KeyboardEvent('keydown',{key:'k',ctrlKey:true}))"><kbd>Ctrl+K</kbd></span>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<div class="journal-subnav">
|
|
||||||
<a href="<?= url_for('journal.php') ?>" class="active">Monatsansicht</a>
|
|
||||||
<a href="<?= url_for('journal_summary.php?year_id=' . $year_id) ?>">Jahressummen</a>
|
|
||||||
<a href="<?= url_for('journal_search.php') ?>">Suche</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($msg): ?><p class="success"><?= htmlspecialchars($msg) ?></p><?php endif; ?>
|
|
||||||
<?php if ($error): ?><p class="error"><?= htmlspecialchars($error) ?></p><?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (empty($years)): ?>
|
|
||||||
<p class="warning">Bitte zuerst ein <a href="<?= url_for('settings.php?tab=journal') ?>">Jahr anlegen</a>.</p>
|
|
||||||
<?php else: ?>
|
|
||||||
|
|
||||||
<!-- Jahr-Dropdown + Monatstabs -->
|
|
||||||
<div class="journal-controls">
|
|
||||||
<form method="get" class="journal-year-select">
|
|
||||||
<label style="margin:0;font-size:10px;">Jahr:
|
|
||||||
<select name="year_id" onchange="this.form.submit();" style="width:auto;min-width:70px;">
|
|
||||||
<?php foreach ($years as $y): ?>
|
|
||||||
<option value="<?= $y['id'] ?>" <?= $y['id'] == $year_id ? 'selected' : '' ?>>
|
|
||||||
<?= (int)$y['year'] ?><?= $y['is_closed'] ? ' (geschl.)' : '' ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<input type="hidden" name="month" value="<?= $month ?>">
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="journal-month-tabs">
|
|
||||||
<?php for ($m = 1; $m <= 12; $m++): ?>
|
|
||||||
<a href="<?= url_for('journal.php?year_id=' . $year_id . '&month=' . $m) ?>"
|
|
||||||
class="<?= $m === $month ? 'active' : '' ?>">
|
|
||||||
<?= journal_month_name($m) ?>
|
|
||||||
</a>
|
|
||||||
<?php endfor; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Monatssummen -->
|
|
||||||
<div class="journal-month-summary">
|
|
||||||
<span class="journal-soll">S: <?= journal_format_amount($totals['_soll'] ?? 0) ?></span>
|
|
||||||
<span class="journal-haben">H: <?= journal_format_amount($totals['_haben'] ?? 0) ?></span>
|
|
||||||
<?php
|
|
||||||
$diff = ($totals['_soll'] ?? 0) - ($totals['_haben'] ?? 0);
|
|
||||||
if (abs($diff) > 0.005):
|
|
||||||
?>
|
|
||||||
<span class="journal-diff-warning">Diff: <?= journal_format_amount($diff) ?></span>
|
|
||||||
<?php endif; ?>
|
|
||||||
<span class="journal-sep"></span>
|
|
||||||
<?php
|
|
||||||
$kasse_balance = ($totals['kasse_s'] ?? 0) - ($totals['kasse_h'] ?? 0);
|
|
||||||
$bank_balance = ($totals['bank_s'] ?? 0) - ($totals['bank_h'] ?? 0);
|
|
||||||
?>
|
|
||||||
<span class="journal-kasse">Kasse: <?= journal_format_amount($kasse_balance) ?></span>
|
|
||||||
<span class="journal-bank">Bank: <?= journal_format_amount($bank_balance) ?></span>
|
|
||||||
<span style="color:var(--text-dim);margin-left:auto;"><?= count($entries) ?> Buchungen</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if (isset($diff) && abs($diff) > 0.005): ?>
|
|
||||||
<div class="journal-diff-banner">
|
|
||||||
<strong>⚠ Buchung nicht ausgeglichen</strong>
|
|
||||||
— Differenz: <strong><?= journal_format_amount($diff) ?></strong>
|
|
||||||
<span style="margin-left:10px;font-size:10px;opacity:0.65;">Soll: <?= journal_format_amount($totals['_soll'] ?? 0) ?> · Haben: <?= journal_format_amount($totals['_haben'] ?? 0) ?></span>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<!-- Journal-Tabelle -->
|
|
||||||
<div class="journal-table-wrap">
|
|
||||||
<table class="journal-table" id="journal-table">
|
|
||||||
<thead>
|
|
||||||
<!-- Gruppenheader -->
|
|
||||||
<tr class="journal-group-header">
|
|
||||||
<th></th>
|
|
||||||
<th colspan="4"></th>
|
|
||||||
<?php foreach ($groups as $gname => $gcount): ?>
|
|
||||||
<th colspan="<?= $gcount ?>"><?= htmlspecialchars($gname) ?></th>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tr>
|
|
||||||
<!-- Spaltenheader -->
|
|
||||||
<tr>
|
|
||||||
<th class="journal-col-action"></th>
|
|
||||||
<th class="journal-col-date">Tag</th>
|
|
||||||
<th class="journal-col-att">B</th>
|
|
||||||
<th class="journal-col-text">Text</th>
|
|
||||||
<th class="journal-col-betrag">Betrag</th>
|
|
||||||
<?php foreach ($columns as $col): ?>
|
|
||||||
<th class="journal-col-amount journal-<?= $col['side'] ?>"><?= htmlspecialchars($col['label']) ?></th>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tr>
|
|
||||||
<!-- Monatssummen oben (wie Excel Zeile 2+3) -->
|
|
||||||
<tr class="journal-top-sums">
|
|
||||||
<td></td>
|
|
||||||
<td colspan="4" style="text-align:right;"><strong>S</strong></td>
|
|
||||||
<?php foreach ($columns as $col): ?>
|
|
||||||
<td class="journal-col-amount journal-<?= $col['side'] ?>">
|
|
||||||
<?php if ($col['side'] === 'soll'): ?>
|
|
||||||
<strong><?= journal_format_amount($totals[$col['key']] ?? 0) ?></strong>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tr>
|
|
||||||
<tr class="journal-top-sums">
|
|
||||||
<td></td>
|
|
||||||
<td colspan="4" style="text-align:right;"><strong>H</strong></td>
|
|
||||||
<?php foreach ($columns as $col): ?>
|
|
||||||
<td class="journal-col-amount journal-<?= $col['side'] ?>">
|
|
||||||
<?php if ($col['side'] === 'haben'): ?>
|
|
||||||
<strong><?= journal_format_amount($totals[$col['key']] ?? 0) ?></strong>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="journal-body">
|
|
||||||
<?php if (empty($entries)): ?>
|
|
||||||
<tr id="empty-row"><td colspan="<?= 6 + count($columns) ?>" style="color:var(--text-dim);padding:6px;">Keine Buchungen.</td></tr>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php foreach ($entries as $entry_row): ?>
|
|
||||||
<tr data-id="<?= $entry_row['id'] ?>">
|
|
||||||
<td class="journal-col-action">
|
|
||||||
<a href="<?= url_for('journal_entry.php?id=' . $entry_row['id']) ?>" title="Bearbeiten">B</a>
|
|
||||||
<a href="#" onclick="deleteEntry(<?= $entry_row['id'] ?>);return false;" title="Löschen">X</a>
|
|
||||||
<a href="#" onclick="duplicateEntry(<?= $entry_row['id'] ?>);return false;" title="Duplizieren">D</a>
|
|
||||||
</td>
|
|
||||||
<td class="journal-col-date"><?= htmlspecialchars(date('d.m', strtotime($entry_row['entry_date']))) ?></td>
|
|
||||||
<td class="journal-col-att"><?= htmlspecialchars($entry_row['attachment_note'] ?? '') ?></td>
|
|
||||||
<td class="journal-col-text">
|
|
||||||
<?= htmlspecialchars($entry_row['description']) ?>
|
|
||||||
<?php if (($entry_row['source_type'] ?? 'manual') === 'invoice_payment' && !empty($entry_row['invoice_id'])): ?>
|
|
||||||
<a href="<?= url_for('invoice_pdf.php?id=' . $entry_row['invoice_id']) ?>" target="_blank" class="source-badge source-invoice" title="Automatisch aus Rechnungszahlung">RE</a>
|
|
||||||
<?php elseif (($entry_row['source_type'] ?? 'manual') === 'expense_payment' && !empty($entry_row['expense_id'])): ?>
|
|
||||||
<a href="<?= url_for('expenses.php?action=edit&id=' . $entry_row['expense_id']) ?>" class="source-badge source-expense" title="Automatisch aus Ausgabe">AU</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<td class="journal-col-betrag"><?= journal_format_amount($entry_row['amount']) ?></td>
|
|
||||||
<?php
|
|
||||||
$row_amounts = [];
|
|
||||||
if (!empty($entry_row['accounts'])) {
|
|
||||||
foreach ($entry_row['accounts'] as $acct) {
|
|
||||||
$key = map_account_to_column_key($acct, $columns);
|
|
||||||
if ($key) {
|
|
||||||
$row_amounts[$key] = ($row_amounts[$key] ?? 0) + (float)$acct['amount'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<?php foreach ($columns as $col): ?>
|
|
||||||
<td class="journal-col-amount journal-<?= $col['side'] ?>">
|
|
||||||
<?= isset($row_amounts[$col['key']]) ? journal_format_amount($row_amounts[$col['key']]) : '' ?>
|
|
||||||
</td>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
<tfoot>
|
|
||||||
<tr class="journal-totals-row">
|
|
||||||
<td></td>
|
|
||||||
<td colspan="4"><strong>Summen</strong></td>
|
|
||||||
<?php foreach ($columns as $col): ?>
|
|
||||||
<td class="journal-col-amount journal-<?= $col['side'] ?>">
|
|
||||||
<strong><?= journal_format_amount($totals[$col['key']] ?? 0) ?></strong>
|
|
||||||
</td>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Inline-Eingabe -->
|
|
||||||
<div id="inline-editor" style="margin-bottom:6px;">
|
|
||||||
<div style="display:flex;gap:4px;align-items:center;margin-bottom:3px;">
|
|
||||||
<button type="button" onclick="toggleInlineForm()" id="btn-new">+ Neue Buchung</button>
|
|
||||||
<a href="<?= url_for('journal_entry.php?year_id=' . $year_id . '&month=' . $month) ?>" style="font-size:10px;color:var(--text-dim);">Vollformular</a>
|
|
||||||
</div>
|
|
||||||
<div id="inline-form" style="display:none;">
|
|
||||||
<form id="inline-entry-form" onsubmit="return saveInlineEntry(event);">
|
|
||||||
<input type="hidden" name="year_id" value="<?= $year_id ?>">
|
|
||||||
<input type="hidden" name="id" id="inline-id" value="">
|
|
||||||
<div style="display:flex;gap:4px;flex-wrap:wrap;margin-bottom:3px;">
|
|
||||||
<?php
|
|
||||||
$last_date = date('Y-m-d');
|
|
||||||
if (!empty($entries)) {
|
|
||||||
$last_entry = end($entries);
|
|
||||||
$last_date = $last_entry['entry_date'];
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<input type="date" name="entry_date" id="ie-date" value="<?= htmlspecialchars($last_date) ?>" required style="width:130px;">
|
|
||||||
<input type="text" name="description" id="ie-desc" placeholder="Text" required style="flex:2;min-width:150px;">
|
|
||||||
<input type="text" name="attachment_note" id="ie-att" placeholder="Beleg" style="width:60px;">
|
|
||||||
<input type="number" step="0.01" name="amount" id="ie-amount" placeholder="Betrag" style="width:80px;">
|
|
||||||
</div>
|
|
||||||
<div class="journal-inline-accounts" id="ie-accounts">
|
|
||||||
<table>
|
|
||||||
<thead><tr><th>Konto</th><th style="width:20px;">S/H</th><th style="width:80px;">Betrag</th><th style="width:80px;">Notiz</th><th style="width:20px;"></th></tr></thead>
|
|
||||||
<tbody id="ie-acct-body"></tbody>
|
|
||||||
</table>
|
|
||||||
<button type="button" onclick="addInlineAccountRow('','soll','','','','',true);" style="padding:1px 6px;font-size:9px;margin-top:2px;">+Konto</button>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex;gap:4px;margin-top:3px;">
|
|
||||||
<button type="submit">Speichern</button>
|
|
||||||
<button type="button" onclick="saveAndNew();" class="secondary">Speichern+Neu</button>
|
|
||||||
<button type="button" onclick="cancelInline();" class="secondary">Abbrechen</button>
|
|
||||||
<span id="inline-status" style="font-size:10px;color:var(--text-dim);align-self:center;"></span>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Umsatzübersicht -->
|
|
||||||
<?php if ($summary_items): ?>
|
|
||||||
<section>
|
|
||||||
<h2>Umsatzübersicht <?= journal_month_name_full($month) ?></h2>
|
|
||||||
<div>
|
|
||||||
<?php
|
|
||||||
$er_cats = get_journal_revenue_categories('erloese', true);
|
|
||||||
foreach ($er_cats as $cat):
|
|
||||||
$cat_key = 'er_' . $cat['id'];
|
|
||||||
$cat_total = $totals[$cat_key] ?? 0;
|
|
||||||
?>
|
|
||||||
<span style="margin-right:12px;font-size:11px;">
|
|
||||||
<?= htmlspecialchars($cat['name']) ?>: <strong><?= journal_format_amount($cat_total) ?></strong>
|
|
||||||
</span>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
|
|
||||||
<form method="post" style="margin-top:4px;">
|
|
||||||
<input type="hidden" name="form" value="summary_values">
|
|
||||||
<input type="hidden" name="year_id" value="<?= $year_id ?>">
|
|
||||||
<input type="hidden" name="month" value="<?= $month ?>">
|
|
||||||
<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:flex-end;">
|
|
||||||
<?php foreach ($summary_items as $item): ?>
|
|
||||||
<label style="margin:0;font-size:10px;"><?= htmlspecialchars($item['name']) ?>:
|
|
||||||
<input type="number" step="0.01" name="summary_val[<?= $item['id'] ?>]"
|
|
||||||
value="<?= htmlspecialchars($summary_values[$item['id']] ?? '0.00') ?>"
|
|
||||||
style="max-width:80px;">
|
|
||||||
</label>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<button type="submit" style="padding:3px 8px;">Speichern</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php endif; ?>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<script src="assets/combobox.js"></script>
|
|
||||||
<script>
|
|
||||||
const accountOptions = <?= $acct_options_json ?>;
|
|
||||||
const yearId = <?= $year_id ?: 0 ?>;
|
|
||||||
const currentMonth = <?= $month ?>;
|
|
||||||
const lastEntryDate = '<?= $last_date ?>';
|
|
||||||
|
|
||||||
function addInlineAccountRow(type, side, amount, revId, expId, note, autoFocus) {
|
|
||||||
type = type || ''; side = side || 'soll'; amount = amount || '';
|
|
||||||
revId = revId || ''; expId = expId || ''; note = note || '';
|
|
||||||
|
|
||||||
const tbody = document.getElementById('ie-acct-body');
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
tr.innerHTML =
|
|
||||||
'<td><div class="pirp-combobox-wrap"></div>' +
|
|
||||||
'<input type="hidden" name="acct_type[]" class="h-type" value="' + type + '">' +
|
|
||||||
'<input type="hidden" name="acct_side[]" class="h-side" value="' + side + '">' +
|
|
||||||
'<input type="hidden" name="acct_rev_id[]" class="h-rev" value="' + revId + '">' +
|
|
||||||
'<input type="hidden" name="acct_exp_id[]" class="h-exp" value="' + expId + '">' +
|
|
||||||
'</td>' +
|
|
||||||
'<td class="side-label" style="text-align:center;font-weight:700;font-size:10px;">' +
|
|
||||||
(side === 'haben' ? 'H' : (type ? 'S' : '')) + '</td>' +
|
|
||||||
'<td><input type="number" step="0.01" name="acct_amount[]" value="' + amount + '" style="width:70px;"></td>' +
|
|
||||||
'<td><input type="text" name="acct_note[]" value="' + note + '" style="width:70px;"></td>' +
|
|
||||||
'<td><a href="#" onclick="this.closest(\'tr\').remove();return false;" style="color:var(--error);font-weight:700;font-size:10px;">X</a></td>';
|
|
||||||
tbody.appendChild(tr);
|
|
||||||
|
|
||||||
const wrap = tr.querySelector('.pirp-combobox-wrap');
|
|
||||||
const selectedValue = type ? (type + '|' + side + '|' + revId + '|' + expId) : '';
|
|
||||||
var cb = new PirpCombobox(wrap, accountOptions, {
|
|
||||||
placeholder: '--Konto--',
|
|
||||||
selectedValue: selectedValue,
|
|
||||||
onSelect: function(opt) {
|
|
||||||
tr.querySelector('.h-type').value = opt.value;
|
|
||||||
tr.querySelector('.h-side').value = opt.side;
|
|
||||||
tr.querySelector('.h-rev').value = opt.rev_id;
|
|
||||||
tr.querySelector('.h-exp').value = opt.exp_id;
|
|
||||||
tr.querySelector('.side-label').textContent = opt.side === 'haben' ? 'H' : 'S';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!type && autoFocus) cb.input.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleInlineForm() {
|
|
||||||
const form = document.getElementById('inline-form');
|
|
||||||
const tableWrap = document.querySelector('.journal-table-wrap');
|
|
||||||
if (form.style.display === 'none') {
|
|
||||||
form.style.display = 'block';
|
|
||||||
resetInlineForm();
|
|
||||||
// Formhöhe messen und Tabelle entsprechend verkleinern
|
|
||||||
const formHeight = form.offsetHeight;
|
|
||||||
tableWrap.style.maxHeight = 'calc(70vh - ' + formHeight + 'px)';
|
|
||||||
document.getElementById('ie-date').focus();
|
|
||||||
} else {
|
|
||||||
form.style.display = 'none';
|
|
||||||
tableWrap.style.maxHeight = '70vh';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetInlineForm() {
|
|
||||||
document.getElementById('inline-id').value = '';
|
|
||||||
document.getElementById('ie-date').value = lastEntryDate;
|
|
||||||
document.getElementById('ie-desc').value = '';
|
|
||||||
document.getElementById('ie-att').value = '';
|
|
||||||
document.getElementById('ie-amount').value = '';
|
|
||||||
document.getElementById('ie-acct-body').innerHTML = '';
|
|
||||||
addInlineAccountRow();
|
|
||||||
addInlineAccountRow();
|
|
||||||
document.getElementById('inline-status').textContent = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelInline() {
|
|
||||||
document.getElementById('inline-form').style.display = 'none';
|
|
||||||
document.querySelector('.journal-table-wrap').style.maxHeight = '70vh';
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveInlineEntry(e) {
|
|
||||||
if (e) e.preventDefault();
|
|
||||||
const form = document.getElementById('inline-entry-form');
|
|
||||||
const fd = new FormData(form);
|
|
||||||
fd.append('action', 'save_entry');
|
|
||||||
|
|
||||||
document.getElementById('inline-status').textContent = 'Speichert...';
|
|
||||||
|
|
||||||
fetch('<?= url_for("journal_api.php") ?>', { method: 'POST', body: fd })
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.ok) {
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
document.getElementById('inline-status').textContent = data.error || 'Fehler';
|
|
||||||
document.getElementById('inline-status').style.color = 'var(--error)';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
document.getElementById('inline-status').textContent = 'Netzwerkfehler';
|
|
||||||
document.getElementById('inline-status').style.color = 'var(--error)';
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveAndNew() {
|
|
||||||
const form = document.getElementById('inline-entry-form');
|
|
||||||
const fd = new FormData(form);
|
|
||||||
fd.append('action', 'save_entry');
|
|
||||||
|
|
||||||
document.getElementById('inline-status').textContent = 'Speichert...';
|
|
||||||
|
|
||||||
fetch('<?= url_for("journal_api.php") ?>', { method: 'POST', body: fd })
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.ok) {
|
|
||||||
// Seite neu laden mit Parameter um Formular offen zu halten
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
url.searchParams.set('new', '1');
|
|
||||||
window.location.href = url.toString();
|
|
||||||
} else {
|
|
||||||
document.getElementById('inline-status').textContent = data.error || 'Fehler';
|
|
||||||
document.getElementById('inline-status').style.color = 'var(--error)';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
document.getElementById('inline-status').textContent = 'Netzwerkfehler';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function pirpConfirm(message, confirmLabel, confirmClass) {
|
|
||||||
confirmLabel = confirmLabel || 'OK';
|
|
||||||
confirmClass = confirmClass || 'danger';
|
|
||||||
return new Promise(function(resolve) {
|
|
||||||
var overlay = document.createElement('div');
|
|
||||||
overlay.className = 'pirp-confirm-overlay';
|
|
||||||
overlay.innerHTML =
|
|
||||||
'<div class="pirp-confirm-box">' +
|
|
||||||
'<p class="pirp-confirm-msg">' + message + '</p>' +
|
|
||||||
'<div class="pirp-confirm-btns">' +
|
|
||||||
'<button class="pirp-confirm-ok ' + confirmClass + '">' + confirmLabel + '</button>' +
|
|
||||||
'<button class="pirp-confirm-cancel secondary">Abbrechen</button>' +
|
|
||||||
'</div>' +
|
|
||||||
'</div>';
|
|
||||||
document.body.appendChild(overlay);
|
|
||||||
|
|
||||||
function cleanup(result) {
|
|
||||||
document.removeEventListener('keydown', onKey);
|
|
||||||
overlay.remove();
|
|
||||||
resolve(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
overlay.querySelector('.pirp-confirm-ok').onclick = function() { cleanup(true); };
|
|
||||||
overlay.querySelector('.pirp-confirm-cancel').onclick = function() { cleanup(false); };
|
|
||||||
overlay.onclick = function(e) { if (e.target === overlay) cleanup(false); };
|
|
||||||
|
|
||||||
function onKey(e) {
|
|
||||||
if (e.key === 'Enter') cleanup(true);
|
|
||||||
if (e.key === 'Escape') cleanup(false);
|
|
||||||
}
|
|
||||||
document.addEventListener('keydown', onKey);
|
|
||||||
overlay.querySelector('.pirp-confirm-cancel').focus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteEntry(id) {
|
|
||||||
const ok = await pirpConfirm('Buchung löschen?', 'Löschen', 'danger');
|
|
||||||
if (!ok) return;
|
|
||||||
const fd = new FormData();
|
|
||||||
fd.append('action', 'delete_entry');
|
|
||||||
fd.append('id', id);
|
|
||||||
|
|
||||||
fetch('<?= url_for("journal_api.php") ?>', { method: 'POST', body: fd })
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.ok) location.reload();
|
|
||||||
else alert(data.error || 'Fehler');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function duplicateEntry(id) {
|
|
||||||
window.location = '<?= url_for("journal_entry.php") ?>?duplicate=' + id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sticky header: top-Werte dynamisch berechnen
|
|
||||||
(function() {
|
|
||||||
const thead = document.querySelector('#journal-table thead');
|
|
||||||
if (!thead) return;
|
|
||||||
const rows = thead.querySelectorAll('tr');
|
|
||||||
let top = 0;
|
|
||||||
rows.forEach(function(row) {
|
|
||||||
row.querySelectorAll('th, td').forEach(function(cell) {
|
|
||||||
cell.style.top = top + 'px';
|
|
||||||
});
|
|
||||||
top += row.offsetHeight;
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Auto-open form wenn ?new=1 in URL (nach Speichern+Neu)
|
|
||||||
if (new URLSearchParams(window.location.search).get('new') === '1') {
|
|
||||||
// URL-Parameter entfernen ohne Reload
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
url.searchParams.delete('new');
|
|
||||||
history.replaceState(null, '', url.toString());
|
|
||||||
// Formular öffnen
|
|
||||||
toggleInlineForm();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script src="assets/command-palette.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_once __DIR__ . '/../src/journal_functions.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
|
|
||||||
$action = $_POST['action'] ?? $_GET['action'] ?? '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
switch ($action) {
|
|
||||||
case 'save_entry':
|
|
||||||
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
|
|
||||||
$data = [
|
|
||||||
'year_id' => (int)$_POST['year_id'],
|
|
||||||
'entry_date' => $_POST['entry_date'] ?? '',
|
|
||||||
'description' => $_POST['description'] ?? '',
|
|
||||||
'attachment_note' => $_POST['attachment_note'] ?? '',
|
|
||||||
'amount' => $_POST['amount'] ?? 0,
|
|
||||||
'supplier_id' => !empty($_POST['supplier_id']) ? (int)$_POST['supplier_id'] : null,
|
|
||||||
'sort_order' => (int)($_POST['sort_order'] ?? 0),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!$data['entry_date'] || !$data['description']) {
|
|
||||||
echo json_encode(['ok' => false, 'error' => 'Datum und Text sind Pflichtfelder.']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$accounts = [];
|
|
||||||
$acct_types = $_POST['acct_type'] ?? [];
|
|
||||||
$acct_sides = $_POST['acct_side'] ?? [];
|
|
||||||
$acct_amounts = $_POST['acct_amount'] ?? [];
|
|
||||||
$acct_rev_ids = $_POST['acct_rev_id'] ?? [];
|
|
||||||
$acct_exp_ids = $_POST['acct_exp_id'] ?? [];
|
|
||||||
$acct_notes = $_POST['acct_note'] ?? [];
|
|
||||||
|
|
||||||
for ($i = 0; $i < count($acct_types); $i++) {
|
|
||||||
if (empty($acct_types[$i]) || (float)($acct_amounts[$i] ?? 0) == 0) continue;
|
|
||||||
$accounts[] = [
|
|
||||||
'account_type' => $acct_types[$i],
|
|
||||||
'side' => $acct_sides[$i] ?? 'soll',
|
|
||||||
'amount' => (float)($acct_amounts[$i] ?? 0),
|
|
||||||
'revenue_category_id' => !empty($acct_rev_ids[$i]) ? (int)$acct_rev_ids[$i] : null,
|
|
||||||
'expense_category_id' => !empty($acct_exp_ids[$i]) ? (int)$acct_exp_ids[$i] : null,
|
|
||||||
'note' => $acct_notes[$i] ?? '',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($accounts)) {
|
|
||||||
echo json_encode(['ok' => false, 'error' => 'Mindestens eine Kontenbuchung erforderlich.']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$saved_id = save_journal_entry($id, $data, $accounts);
|
|
||||||
echo json_encode(['ok' => true, 'id' => $saved_id]);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'delete_entry':
|
|
||||||
$del_id = (int)($_POST['id'] ?? 0);
|
|
||||||
if ($del_id) {
|
|
||||||
delete_journal_entry($del_id);
|
|
||||||
echo json_encode(['ok' => true]);
|
|
||||||
} else {
|
|
||||||
echo json_encode(['ok' => false, 'error' => 'Keine ID.']);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'get_entry':
|
|
||||||
$get_id = (int)($_GET['id'] ?? 0);
|
|
||||||
$entry = get_journal_entry($get_id);
|
|
||||||
echo json_encode(['ok' => true, 'entry' => $entry]);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
echo json_encode(['ok' => false, 'error' => 'Unbekannte Aktion.']);
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
echo json_encode(['ok' => false, 'error' => $e->getMessage()]);
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_once __DIR__ . '/../src/journal_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/icons.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
$year_id = isset($_GET['year_id']) ? (int)$_GET['year_id'] : 0;
|
|
||||||
$month = isset($_GET['month']) ? (int)$_GET['month'] : 0;
|
|
||||||
$col_key = $_GET['col_key'] ?? '';
|
|
||||||
|
|
||||||
if (!$year_id || !$month || !$col_key) {
|
|
||||||
header('Location: ' . url_for('journal_summary.php'));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Jahr laden
|
|
||||||
$years = get_journal_years();
|
|
||||||
$current_year = null;
|
|
||||||
foreach ($years as $y) {
|
|
||||||
if ($y['id'] == $year_id) { $current_year = $y; break; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spalte finden
|
|
||||||
$columns = get_journal_columns();
|
|
||||||
$col = null;
|
|
||||||
foreach ($columns as $c) {
|
|
||||||
if ($c['key'] === $col_key) { $col = $c; break; }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$col) {
|
|
||||||
header('Location: ' . url_for('journal_summary.php?year_id=' . $year_id));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// SQL aufbauen
|
|
||||||
$pdo = get_db();
|
|
||||||
|
|
||||||
$where = "e.year_id = :year_id AND e.month = :month AND a.account_type = :account_type AND a.side = :side";
|
|
||||||
$params = [
|
|
||||||
':year_id' => $year_id,
|
|
||||||
':month' => $month,
|
|
||||||
':account_type' => $col['account_type'],
|
|
||||||
':side' => $col['side'],
|
|
||||||
];
|
|
||||||
|
|
||||||
if (($col['account_type'] === 'wareneingang' || $col['account_type'] === 'erloese') && $col['category_id']) {
|
|
||||||
$where .= " AND a.revenue_category_id = :rev_cat_id";
|
|
||||||
$params[':rev_cat_id'] = $col['category_id'];
|
|
||||||
} elseif (($col['account_type'] === 'expense' || $col['account_type'] === 'deduction') && $col['category_id']) {
|
|
||||||
$where .= " AND a.expense_category_id = :exp_cat_id";
|
|
||||||
$params[':exp_cat_id'] = $col['category_id'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql = "SELECT e.id, e.entry_date, e.description, e.attachment_note, e.amount AS entry_amount,
|
|
||||||
a.amount AS col_amount, a.note AS acct_note,
|
|
||||||
s.name AS supplier_name
|
|
||||||
FROM journal_entries e
|
|
||||||
JOIN journal_entry_accounts a ON a.entry_id = e.id
|
|
||||||
LEFT JOIN journal_suppliers s ON e.supplier_id = s.id
|
|
||||||
WHERE $where
|
|
||||||
ORDER BY e.entry_date, e.sort_order, e.id";
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
$entries = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
$total = array_sum(array_column($entries, 'col_amount'));
|
|
||||||
|
|
||||||
$month_name = journal_month_name_full($month);
|
|
||||||
$year_val = $current_year ? (int)$current_year['year'] : $year_id;
|
|
||||||
$back_url = url_for('journal_summary.php?year_id=' . $year_id);
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title><?= htmlspecialchars($col['label']) ?> – <?= $month_name ?> <?= $year_val ?></title>
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>PIRP</h1>
|
|
||||||
<nav>
|
|
||||||
<a href="<?= url_for('index.php') ?>"><?= icon_dashboard() ?>Dashboard</a>
|
|
||||||
<a href="<?= url_for('invoices.php') ?>"><?= icon_invoices() ?>Rechnungen</a>
|
|
||||||
<a href="<?= url_for('customers.php') ?>"><?= icon_customers() ?>Kunden</a>
|
|
||||||
<a href="<?= url_for('expenses.php') ?>"><?= icon_expenses() ?>Ausgaben</a>
|
|
||||||
<a href="<?= url_for('belegarchiv.php') ?>"><?= icon_archive() ?>Belege</a>
|
|
||||||
<a href="<?= url_for('journal.php') ?>" class="active"><?= icon_journal() ?>Journal</a>
|
|
||||||
<a href="<?= url_for('euer.php') ?>"><?= icon_euer() ?>EÜR</a>
|
|
||||||
<a href="<?= url_for('settings.php') ?>"><?= icon_settings() ?>Einstellungen</a>
|
|
||||||
<a href="<?= url_for('logout.php') ?>"><?= icon_logout() ?>Logout (<?= htmlspecialchars($_SESSION['username'] ?? '') ?>)</a>
|
|
||||||
<span class="cmd-k-hint" onclick="document.dispatchEvent(new KeyboardEvent('keydown',{key:'k',ctrlKey:true}))"><kbd>Ctrl+K</kbd></span>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<div class="journal-subnav">
|
|
||||||
<a href="<?= url_for('journal.php?year_id=' . $year_id) ?>">Monatsansicht</a>
|
|
||||||
<a href="<?= $back_url ?>">Jahressummen</a>
|
|
||||||
<a href="<?= url_for('journal_search.php') ?>">Suche</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-bottom:8px;">
|
|
||||||
<a href="<?= $back_url ?>" style="font-size:11px;color:var(--text-dim);">← Zurück zu Jahressummen</a>
|
|
||||||
<a href="<?= url_for('journal_search.php') ?>">Suche</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>
|
|
||||||
<?= htmlspecialchars($col['label']) ?>
|
|
||||||
<span style="font-weight:normal;font-size:13px;color:var(--text-dim);">
|
|
||||||
– <?= $month_name ?> <?= $year_val ?>
|
|
||||||
(<?= htmlspecialchars($col['group']) ?>, <?= $col['side'] === 'soll' ? 'Soll' : 'Haben' ?>)
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<?php if (empty($entries)): ?>
|
|
||||||
<p style="color:var(--text-dim);">Keine Buchungen in diesem Zeitraum.</p>
|
|
||||||
<?php else: ?>
|
|
||||||
<div class="journal-table-wrap">
|
|
||||||
<table class="journal-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="journal-col-date">Datum</th>
|
|
||||||
<th class="journal-col-att">B</th>
|
|
||||||
<th class="journal-col-text">Text</th>
|
|
||||||
<th class="journal-col-betrag">Buchungsbetrag</th>
|
|
||||||
<th class="journal-col-amount journal-<?= $col['side'] ?>"><?= htmlspecialchars($col['label']) ?></th>
|
|
||||||
<th class="journal-col-text" style="min-width:80px;">Notiz</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($entries as $row): ?>
|
|
||||||
<tr>
|
|
||||||
<td class="journal-col-date"><?= htmlspecialchars(date('d.m.Y', strtotime($row['entry_date']))) ?></td>
|
|
||||||
<td class="journal-col-att"><?= htmlspecialchars($row['attachment_note'] ?? '') ?></td>
|
|
||||||
<td class="journal-col-text">
|
|
||||||
<a href="<?= url_for('journal_entry.php?id=' . $row['id']) ?>" style="color:inherit;">
|
|
||||||
<?= htmlspecialchars($row['description']) ?>
|
|
||||||
</a>
|
|
||||||
<?php if ($row['supplier_name']): ?>
|
|
||||||
<span style="color:var(--text-dim);font-size:10px;"> · <?= htmlspecialchars($row['supplier_name']) ?></span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<td class="journal-col-betrag"><?= journal_format_amount((float)$row['entry_amount']) ?></td>
|
|
||||||
<td class="journal-col-amount journal-<?= $col['side'] ?>"><?= journal_format_amount((float)$row['col_amount']) ?></td>
|
|
||||||
<td style="font-size:10px;color:var(--text-dim);"><?= htmlspecialchars($row['acct_note'] ?? '') ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
<tfoot>
|
|
||||||
<tr class="journal-totals-row">
|
|
||||||
<td colspan="4"><strong>Summe</strong></td>
|
|
||||||
<td class="journal-col-amount journal-<?= $col['side'] ?>"><strong><?= journal_format_amount($total) ?></strong></td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<p style="font-size:11px;color:var(--text-dim);margin-top:4px;"><?= count($entries) ?> Buchung(en)</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<script src="assets/command-palette.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_once __DIR__ . '/../src/journal_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/icons.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
$msg = '';
|
|
||||||
$error = '';
|
|
||||||
|
|
||||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
|
||||||
$year_id = isset($_GET['year_id']) ? (int)$_GET['year_id'] : null;
|
|
||||||
$month = isset($_GET['month']) ? (int)$_GET['month'] : (int)date('n');
|
|
||||||
$save_and_new = false;
|
|
||||||
|
|
||||||
// Duplizieren
|
|
||||||
if (isset($_GET['duplicate'])) {
|
|
||||||
$source_id = (int)$_GET['duplicate'];
|
|
||||||
try {
|
|
||||||
$new_id = duplicate_journal_entry($source_id);
|
|
||||||
header('Location: ' . url_for('journal_entry.php?id=' . $new_id));
|
|
||||||
exit;
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$error = 'Fehler beim Duplizieren: ' . $e->getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$entry = null;
|
|
||||||
if ($id) {
|
|
||||||
$entry = get_journal_entry($id);
|
|
||||||
if ($entry) {
|
|
||||||
$year_id = $entry['year_id'];
|
|
||||||
$month = $entry['month'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST: Speichern
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$year_id = (int)$_POST['year_id'];
|
|
||||||
$save_and_new = isset($_POST['save_and_new']);
|
|
||||||
$id_post = !empty($_POST['id']) ? (int)$_POST['id'] : null;
|
|
||||||
|
|
||||||
$data = [
|
|
||||||
'year_id' => $year_id,
|
|
||||||
'entry_date' => $_POST['entry_date'] ?? '',
|
|
||||||
'description' => $_POST['description'] ?? '',
|
|
||||||
'attachment_note' => $_POST['attachment_note'] ?? '',
|
|
||||||
'amount' => $_POST['amount'] ?? 0,
|
|
||||||
'supplier_id' => $_POST['supplier_id'] ?? null,
|
|
||||||
'sort_order' => $_POST['sort_order'] ?? 0,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!$data['entry_date'] || !$data['description']) {
|
|
||||||
$error = 'Datum und Text sind Pflichtfelder.';
|
|
||||||
} else {
|
|
||||||
// Kontenverteilung aus POST parsen
|
|
||||||
$accounts = [];
|
|
||||||
$acct_types = $_POST['acct_type'] ?? [];
|
|
||||||
$acct_sides = $_POST['acct_side'] ?? [];
|
|
||||||
$acct_amounts = $_POST['acct_amount'] ?? [];
|
|
||||||
$acct_rev_ids = $_POST['acct_rev_id'] ?? [];
|
|
||||||
$acct_exp_ids = $_POST['acct_exp_id'] ?? [];
|
|
||||||
$acct_notes = $_POST['acct_note'] ?? [];
|
|
||||||
|
|
||||||
for ($i = 0; $i < count($acct_types); $i++) {
|
|
||||||
if (empty($acct_types[$i]) || (float)($acct_amounts[$i] ?? 0) == 0) continue;
|
|
||||||
$accounts[] = [
|
|
||||||
'account_type' => $acct_types[$i],
|
|
||||||
'side' => $acct_sides[$i] ?? 'soll',
|
|
||||||
'amount' => (float)($acct_amounts[$i] ?? 0),
|
|
||||||
'revenue_category_id' => !empty($acct_rev_ids[$i]) ? (int)$acct_rev_ids[$i] : null,
|
|
||||||
'expense_category_id' => !empty($acct_exp_ids[$i]) ? (int)$acct_exp_ids[$i] : null,
|
|
||||||
'note' => $acct_notes[$i] ?? '',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($accounts)) {
|
|
||||||
$error = 'Mindestens eine Kontenbuchung ist erforderlich.';
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
$saved_id = save_journal_entry($id_post, $data, $accounts);
|
|
||||||
if ($save_and_new) {
|
|
||||||
$month = (int)date('n', strtotime($data['entry_date']));
|
|
||||||
header('Location: ' . url_for('journal_entry.php?year_id=' . $year_id . '&month=' . $month . '&msg=gespeichert'));
|
|
||||||
exit;
|
|
||||||
} else {
|
|
||||||
header('Location: ' . url_for('journal.php?year_id=' . $year_id . '&month=' . (int)date('n', strtotime($data['entry_date']))));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$error = 'Fehler beim Speichern: ' . $e->getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_GET['msg'])) {
|
|
||||||
$msg = 'Buchung gespeichert.';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Daten laden
|
|
||||||
$years = get_journal_years();
|
|
||||||
$account_options = get_journal_account_options();
|
|
||||||
|
|
||||||
// Wenn kein year_id, das erste offene Jahr nehmen
|
|
||||||
if (!$year_id && $years) {
|
|
||||||
foreach ($years as $y) {
|
|
||||||
if (!$y['is_closed']) {
|
|
||||||
$year_id = $y['id'];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!$year_id) $year_id = $years[0]['id'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$current_year = null;
|
|
||||||
foreach ($years as $y) {
|
|
||||||
if ($y['id'] == $year_id) {
|
|
||||||
$current_year = $y;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Konto-Optionen als JSON für JavaScript
|
|
||||||
$acct_options_json = json_encode($account_options);
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Buchung <?= $entry ? 'bearbeiten' : 'erstellen' ?></title>
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
<script src="assets/combobox.js"></script>
|
|
||||||
<script>
|
|
||||||
const accountOptions = <?= $acct_options_json ?>;
|
|
||||||
|
|
||||||
function addAccountRow(type, side, amount, revId, expId, note) {
|
|
||||||
type = type || '';
|
|
||||||
side = side || 'soll';
|
|
||||||
amount = amount || '';
|
|
||||||
revId = revId || '';
|
|
||||||
expId = expId || '';
|
|
||||||
note = note || '';
|
|
||||||
|
|
||||||
const tbody = document.getElementById('accounts-body');
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
tr.innerHTML =
|
|
||||||
'<td><div class="pirp-combobox-wrap"></div>' +
|
|
||||||
'<input type="hidden" name="acct_type[]" class="acct-type-hidden" value="' + type + '">' +
|
|
||||||
'<input type="hidden" name="acct_side[]" class="acct-side-hidden" value="' + side + '">' +
|
|
||||||
'<input type="hidden" name="acct_rev_id[]" class="acct-rev-hidden" value="' + revId + '">' +
|
|
||||||
'<input type="hidden" name="acct_exp_id[]" class="acct-exp-hidden" value="' + expId + '">' +
|
|
||||||
'</td>' +
|
|
||||||
'<td class="acct-side-display">' + (side === 'haben' ? 'H' : (type ? 'S' : '')) + '</td>' +
|
|
||||||
'<td><input type="number" step="0.01" name="acct_amount[]" value="' + amount + '" style="max-width:120px;"></td>' +
|
|
||||||
'<td><input type="text" name="acct_note[]" value="' + note + '" style="max-width:150px;"></td>' +
|
|
||||||
'<td><button type="button" class="danger" style="padding:2px 8px;font-size:10px;" onclick="this.closest(\'tr\').remove();">X</button></td>';
|
|
||||||
tbody.appendChild(tr);
|
|
||||||
|
|
||||||
const wrap = tr.querySelector('.pirp-combobox-wrap');
|
|
||||||
const selectedValue = type ? (type + '|' + side + '|' + revId + '|' + expId) : '';
|
|
||||||
new PirpCombobox(wrap, accountOptions, {
|
|
||||||
placeholder: '-- Konto wählen --',
|
|
||||||
selectedValue: selectedValue,
|
|
||||||
onSelect: function(opt) {
|
|
||||||
tr.querySelector('.acct-type-hidden').value = opt.value;
|
|
||||||
tr.querySelector('.acct-side-hidden').value = opt.side;
|
|
||||||
tr.querySelector('.acct-rev-hidden').value = opt.rev_id;
|
|
||||||
tr.querySelector('.acct-exp-hidden').value = opt.exp_id;
|
|
||||||
tr.querySelector('.acct-side-display').textContent = opt.side === 'soll' ? 'S' : 'H';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const form = document.querySelector('form');
|
|
||||||
form.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA' && e.target.type !== 'submit') {
|
|
||||||
e.preventDefault();
|
|
||||||
const inputs = Array.from(form.querySelectorAll('input:not([type="hidden"]), select, textarea, button[type="submit"]'));
|
|
||||||
const currentIndex = inputs.indexOf(e.target);
|
|
||||||
if (currentIndex > -1 && currentIndex < inputs.length - 1) {
|
|
||||||
inputs[currentIndex + 1].focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>PIRP</h1>
|
|
||||||
<nav>
|
|
||||||
<a href="<?= url_for('index.php') ?>"><?= icon_dashboard() ?>Dashboard</a>
|
|
||||||
<a href="<?= url_for('invoices.php') ?>"><?= icon_invoices() ?>Rechnungen</a>
|
|
||||||
<a href="<?= url_for('customers.php') ?>"><?= icon_customers() ?>Kunden</a>
|
|
||||||
<a href="<?= url_for('expenses.php') ?>"><?= icon_expenses() ?>Ausgaben</a>
|
|
||||||
<a href="<?= url_for('belegarchiv.php') ?>"><?= icon_archive() ?>Belege</a>
|
|
||||||
<a href="<?= url_for('journal.php') ?>" class="active"><?= icon_journal() ?>Journal</a>
|
|
||||||
<a href="<?= url_for('euer.php') ?>"><?= icon_euer() ?>EÜR</a>
|
|
||||||
<a href="<?= url_for('settings.php') ?>"><?= icon_settings() ?>Einstellungen</a>
|
|
||||||
<a href="<?= url_for('logout.php') ?>"><?= icon_logout() ?>Logout (<?= htmlspecialchars($_SESSION['username'] ?? '') ?>)</a>
|
|
||||||
<span class="cmd-k-hint" onclick="document.dispatchEvent(new KeyboardEvent('keydown',{key:'k',ctrlKey:true}))"><kbd>Ctrl+K</kbd></span>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<div class="journal-subnav">
|
|
||||||
<a href="<?= url_for('journal.php') ?>">Monatsansicht</a>
|
|
||||||
<a href="<?= url_for('journal_summary.php') ?>">Jahressummen</a>
|
|
||||||
<a href="<?= url_for('journal_search.php') ?>">Suche</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($msg): ?><p class="success"><?= htmlspecialchars($msg) ?></p><?php endif; ?>
|
|
||||||
<?php if ($error): ?><p class="error"><?= htmlspecialchars($error) ?></p><?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (empty($years)): ?>
|
|
||||||
<p class="warning">Bitte zuerst ein <a href="<?= url_for('settings.php?tab=journal') ?>">Jahr anlegen</a>.</p>
|
|
||||||
<?php else: ?>
|
|
||||||
<section>
|
|
||||||
<h2><?= $entry ? 'Buchung bearbeiten' : 'Neue Buchung' ?></h2>
|
|
||||||
<?php if ($entry && !empty($entry['source_type']) && $entry['source_type'] !== 'manual'): ?>
|
|
||||||
<p style="font-size:10px;color:var(--accent);margin-bottom:4px;">
|
|
||||||
<?php if ($entry['source_type'] === 'invoice_payment' && !empty($entry['invoice_id'])): ?>
|
|
||||||
Automatisch erstellt aus Rechnungszahlung:
|
|
||||||
<a href="<?= url_for('invoice_pdf.php?id=' . $entry['invoice_id']) ?>" target="_blank">Rechnung PDF</a>
|
|
||||||
<?php elseif ($entry['source_type'] === 'expense_payment' && !empty($entry['expense_id'])): ?>
|
|
||||||
Automatisch erstellt aus Ausgabe:
|
|
||||||
<a href="<?= url_for('expenses.php?action=edit&id=' . $entry['expense_id']) ?>">Ausgabe bearbeiten</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
<form method="post">
|
|
||||||
<input type="hidden" name="id" value="<?= htmlspecialchars($entry['id'] ?? '') ?>">
|
|
||||||
|
|
||||||
<div class="flex-row">
|
|
||||||
<label>Jahr:
|
|
||||||
<select name="year_id" required>
|
|
||||||
<?php foreach ($years as $y): ?>
|
|
||||||
<option value="<?= $y['id'] ?>" <?= $y['id'] == $year_id ? 'selected' : '' ?> <?= $y['is_closed'] ? 'disabled' : '' ?>>
|
|
||||||
<?= (int)$y['year'] ?><?= $y['is_closed'] ? ' (geschlossen)' : '' ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>Datum:
|
|
||||||
<input type="date" name="entry_date" value="<?= htmlspecialchars($entry['entry_date'] ?? date('Y-m-d')) ?>" required>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<label>Text:
|
|
||||||
<input type="text" name="description" value="<?= htmlspecialchars($entry['description'] ?? '') ?>" required style="max-width:600px;">
|
|
||||||
</label>
|
|
||||||
<label>Beleg:
|
|
||||||
<input type="text" name="attachment_note" value="<?= htmlspecialchars($entry['attachment_note'] ?? '') ?>" style="max-width:200px;">
|
|
||||||
</label>
|
|
||||||
<div class="flex-row">
|
|
||||||
<label>Betrag:
|
|
||||||
<input type="number" step="0.01" name="amount" value="<?= htmlspecialchars($entry['amount'] ?? '0.00') ?>" required style="max-width:150px;">
|
|
||||||
</label>
|
|
||||||
<label>Sortierung:
|
|
||||||
<input type="number" name="sort_order" value="<?= htmlspecialchars($entry['sort_order'] ?? '0') ?>" style="max-width:80px;">
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Kontenverteilung</h2>
|
|
||||||
<table class="list">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Konto</th>
|
|
||||||
<th>S/H</th>
|
|
||||||
<th>Betrag</th>
|
|
||||||
<th>Notiz</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="accounts-body">
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<button type="button" onclick="addAccountRow();" style="margin-top:8px;">+ Zeile hinzufügen</button>
|
|
||||||
|
|
||||||
<div style="margin-top:16px;">
|
|
||||||
<button type="submit">Speichern</button>
|
|
||||||
<button type="submit" name="save_and_new" value="1" class="secondary">Speichern & Neu</button>
|
|
||||||
<a href="<?= url_for('journal.php?year_id=' . $year_id . '&month=' . $month) ?>" class="button-secondary">Abbrechen</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Bestehende Konten laden oder leere Zeilen
|
|
||||||
<?php if ($entry && !empty($entry['accounts'])): ?>
|
|
||||||
<?php foreach ($entry['accounts'] as $acct): ?>
|
|
||||||
addAccountRow(
|
|
||||||
<?= json_encode($acct['account_type']) ?>,
|
|
||||||
<?= json_encode($acct['side']) ?>,
|
|
||||||
<?= json_encode($acct['amount']) ?>,
|
|
||||||
<?= json_encode($acct['revenue_category_id'] ?? '') ?>,
|
|
||||||
<?= json_encode($acct['expense_category_id'] ?? '') ?>,
|
|
||||||
<?= json_encode($acct['note'] ?? '') ?>
|
|
||||||
);
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php else: ?>
|
|
||||||
addAccountRow();
|
|
||||||
addAccountRow();
|
|
||||||
<?php endif; ?>
|
|
||||||
</script>
|
|
||||||
<?php endif; ?>
|
|
||||||
</main>
|
|
||||||
<script src="assets/command-palette.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Redirect to new unified EÜR page
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/journal_functions.php';
|
|
||||||
|
|
||||||
// Get year from year_id parameter for backwards compatibility
|
|
||||||
$year = date('Y');
|
|
||||||
if (isset($_GET['year_id'])) {
|
|
||||||
$year_id = (int)$_GET['year_id'];
|
|
||||||
$years = get_journal_years();
|
|
||||||
foreach ($years as $y) {
|
|
||||||
if ($y['id'] == $year_id) {
|
|
||||||
$year = (int)$y['year'];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
header('Location: ' . url_for('euer.php?year=' . $year));
|
|
||||||
exit;
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_once __DIR__ . '/../src/journal_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/icons.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
$q = trim($_GET['q'] ?? '');
|
|
||||||
$from = trim($_GET['from'] ?? '');
|
|
||||||
$to = trim($_GET['to'] ?? '');
|
|
||||||
$year_id = isset($_GET['year_id']) ? (int)$_GET['year_id'] : 0;
|
|
||||||
|
|
||||||
$years = get_journal_years();
|
|
||||||
|
|
||||||
$entries = [];
|
|
||||||
$searched = false;
|
|
||||||
|
|
||||||
if ($q !== '' || $from !== '' || $to !== '' || $year_id) {
|
|
||||||
$searched = true;
|
|
||||||
$pdo = get_db();
|
|
||||||
|
|
||||||
$sql = "SELECT e.id, e.entry_date, e.month, e.description, e.attachment_note,
|
|
||||||
e.amount, e.year_id, y.year AS journal_year,
|
|
||||||
s.name AS supplier_name
|
|
||||||
FROM journal_entries e
|
|
||||||
JOIN journal_years y ON y.id = e.year_id
|
|
||||||
LEFT JOIN journal_suppliers s ON s.id = e.supplier_id
|
|
||||||
WHERE 1=1";
|
|
||||||
$params = [];
|
|
||||||
|
|
||||||
if ($q !== '') {
|
|
||||||
$sql .= " AND (e.description ILIKE :q OR e.attachment_note ILIKE :q2)";
|
|
||||||
$params[':q'] = '%' . $q . '%';
|
|
||||||
$params[':q2'] = '%' . $q . '%';
|
|
||||||
}
|
|
||||||
if ($from !== '') {
|
|
||||||
$sql .= " AND e.entry_date >= :from";
|
|
||||||
$params[':from'] = $from;
|
|
||||||
}
|
|
||||||
if ($to !== '') {
|
|
||||||
$sql .= " AND e.entry_date <= :to";
|
|
||||||
$params[':to'] = $to;
|
|
||||||
}
|
|
||||||
if ($year_id) {
|
|
||||||
$sql .= " AND e.year_id = :year_id";
|
|
||||||
$params[':year_id'] = $year_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql .= " ORDER BY e.entry_date DESC, e.id DESC LIMIT 200";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
$entries = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Journal-Suche</title>
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>PIRP</h1>
|
|
||||||
<nav>
|
|
||||||
<a href="<?= url_for('index.php') ?>"><?= icon_dashboard() ?>Dashboard</a>
|
|
||||||
<a href="<?= url_for('invoices.php') ?>"><?= icon_invoices() ?>Rechnungen</a>
|
|
||||||
<a href="<?= url_for('customers.php') ?>"><?= icon_customers() ?>Kunden</a>
|
|
||||||
<a href="<?= url_for('expenses.php') ?>"><?= icon_expenses() ?>Ausgaben</a>
|
|
||||||
<a href="<?= url_for('belegarchiv.php') ?>"><?= icon_archive() ?>Belege</a>
|
|
||||||
<a href="<?= url_for('journal.php') ?>" class="active"><?= icon_journal() ?>Journal</a>
|
|
||||||
<a href="<?= url_for('euer.php') ?>"><?= icon_euer() ?>EÜR</a>
|
|
||||||
<a href="<?= url_for('settings.php') ?>"><?= icon_settings() ?>Einstellungen</a>
|
|
||||||
<a href="<?= url_for('logout.php') ?>"><?= icon_logout() ?>Logout (<?= htmlspecialchars($_SESSION['username'] ?? '') ?>)</a>
|
|
||||||
<span class="cmd-k-hint" onclick="document.dispatchEvent(new KeyboardEvent('keydown',{key:'k',ctrlKey:true}))"><kbd>Ctrl+K</kbd></span>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<div class="journal-subnav">
|
|
||||||
<a href="<?= url_for('journal.php') ?>">Monatsansicht</a>
|
|
||||||
<a href="<?= url_for('journal_summary.php') ?>">Jahressummen</a>
|
|
||||||
<a href="<?= url_for('journal_search.php') ?>" class="active">Suche</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form method="get" class="filters">
|
|
||||||
<label>Suche:
|
|
||||||
<input type="text" name="q" value="<?= htmlspecialchars($q) ?>" placeholder="Beschreibung, Beleg..." autofocus>
|
|
||||||
</label>
|
|
||||||
<label>Von:
|
|
||||||
<input type="date" name="from" value="<?= htmlspecialchars($from) ?>">
|
|
||||||
</label>
|
|
||||||
<label>Bis:
|
|
||||||
<input type="date" name="to" value="<?= htmlspecialchars($to) ?>">
|
|
||||||
</label>
|
|
||||||
<?php if ($years): ?>
|
|
||||||
<label>Jahr:
|
|
||||||
<select name="year_id">
|
|
||||||
<option value="">Alle Jahre</option>
|
|
||||||
<?php foreach ($years as $y): ?>
|
|
||||||
<option value="<?= $y['id'] ?>" <?= $y['id'] == $year_id ? 'selected' : '' ?>><?= (int)$y['year'] ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<?php endif; ?>
|
|
||||||
<button type="submit">Suchen</button>
|
|
||||||
<a href="<?= url_for('journal_search.php') ?>">Zurücksetzen</a>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<?php if ($searched): ?>
|
|
||||||
<section>
|
|
||||||
<h2>Suchergebnisse <?php if ($entries): ?><span style="font-weight:normal;font-size:12px;color:var(--text-muted);">(<?= count($entries) ?> Treffer<?= count($entries) >= 200 ? ', max. 200' : '' ?>)</span><?php endif; ?></h2>
|
|
||||||
|
|
||||||
<?php if (empty($entries)): ?>
|
|
||||||
<p style="color:var(--text-muted);">Keine Buchungen gefunden.</p>
|
|
||||||
<?php else: ?>
|
|
||||||
<div class="journal-table-wrap">
|
|
||||||
<table class="journal-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="journal-col-date">Datum</th>
|
|
||||||
<th class="journal-col-att">B</th>
|
|
||||||
<th class="journal-col-text">Beschreibung</th>
|
|
||||||
<th class="journal-col-betrag">Betrag</th>
|
|
||||||
<th>Jahr / Monat</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($entries as $e): ?>
|
|
||||||
<tr>
|
|
||||||
<td class="journal-col-date"><?= date('d.m.Y', strtotime($e['entry_date'])) ?></td>
|
|
||||||
<td class="journal-col-att"><?= htmlspecialchars($e['attachment_note'] ?? '') ?></td>
|
|
||||||
<td class="journal-col-text">
|
|
||||||
<a href="<?= url_for('journal_entry.php?id=' . $e['id']) ?>" style="color:inherit;">
|
|
||||||
<?= htmlspecialchars($e['description']) ?>
|
|
||||||
</a>
|
|
||||||
<?php if ($e['supplier_name']): ?>
|
|
||||||
<span style="color:var(--text-dim);font-size:10px;"> · <?= htmlspecialchars($e['supplier_name']) ?></span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<td class="journal-col-betrag"><?= number_format((float)$e['amount'], 2, ',', '.') ?></td>
|
|
||||||
<td>
|
|
||||||
<a href="<?= url_for('journal.php?year_id=' . $e['year_id'] . '&month=' . $e['month']) ?>"
|
|
||||||
style="font-size:11px;color:var(--text-muted);">
|
|
||||||
<?= (int)$e['journal_year'] ?> / <?= journal_month_name_full((int)$e['month']) ?>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
|
||||||
</main>
|
|
||||||
<script src="assets/command-palette.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Redirect to main settings with journal tab
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
header('Location: ' . url_for('settings.php?tab=journal'));
|
|
||||||
exit;
|
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_once __DIR__ . '/../src/journal_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/icons.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
$years = get_journal_years();
|
|
||||||
|
|
||||||
$year_id = isset($_GET['year_id']) ? (int)$_GET['year_id'] : null;
|
|
||||||
if (!$year_id && $years) {
|
|
||||||
$current_cal_year = (int)date('Y');
|
|
||||||
foreach ($years as $y) {
|
|
||||||
if ($y['year'] == $current_cal_year) {
|
|
||||||
$year_id = $y['id'];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!$year_id) $year_id = $years[0]['id'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$current_year = null;
|
|
||||||
foreach ($years as $y) {
|
|
||||||
if ($y['id'] == $year_id) {
|
|
||||||
$current_year = $y;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$summary = null;
|
|
||||||
$profitability = null;
|
|
||||||
if ($year_id) {
|
|
||||||
$summary = calculate_yearly_summary($year_id);
|
|
||||||
$profitability = calculate_yearly_profitability($year_id);
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Journal Jahresübersicht <?= $current_year ? (int)$current_year['year'] : '' ?></title>
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>PIRP</h1>
|
|
||||||
<nav>
|
|
||||||
<a href="<?= url_for('index.php') ?>"><?= icon_dashboard() ?>Dashboard</a>
|
|
||||||
<a href="<?= url_for('invoices.php') ?>"><?= icon_invoices() ?>Rechnungen</a>
|
|
||||||
<a href="<?= url_for('customers.php') ?>"><?= icon_customers() ?>Kunden</a>
|
|
||||||
<a href="<?= url_for('expenses.php') ?>"><?= icon_expenses() ?>Ausgaben</a>
|
|
||||||
<a href="<?= url_for('belegarchiv.php') ?>"><?= icon_archive() ?>Belege</a>
|
|
||||||
<a href="<?= url_for('journal.php') ?>" class="active"><?= icon_journal() ?>Journal</a>
|
|
||||||
<a href="<?= url_for('euer.php') ?>"><?= icon_euer() ?>EÜR</a>
|
|
||||||
<a href="<?= url_for('settings.php') ?>"><?= icon_settings() ?>Einstellungen</a>
|
|
||||||
<a href="<?= url_for('logout.php') ?>"><?= icon_logout() ?>Logout (<?= htmlspecialchars($_SESSION['username'] ?? '') ?>)</a>
|
|
||||||
<span class="cmd-k-hint" onclick="document.dispatchEvent(new KeyboardEvent('keydown',{key:'k',ctrlKey:true}))"><kbd>Ctrl+K</kbd></span>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<div class="journal-subnav">
|
|
||||||
<a href="<?= url_for('journal.php?year_id=' . $year_id) ?>">Monatsansicht</a>
|
|
||||||
<a href="<?= url_for('journal_summary.php') ?>" class="active">Jahressummen</a>
|
|
||||||
<a href="<?= url_for('journal_search.php') ?>">Suche</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if (empty($years)): ?>
|
|
||||||
<p class="warning">Bitte zuerst ein <a href="<?= url_for('settings.php?tab=journal') ?>">Jahr anlegen</a>.</p>
|
|
||||||
<?php elseif ($summary): ?>
|
|
||||||
|
|
||||||
<!-- Jahr-Dropdown -->
|
|
||||||
<div class="journal-controls">
|
|
||||||
<form method="get" class="journal-year-select">
|
|
||||||
<label>Jahr:
|
|
||||||
<select name="year_id" onchange="this.form.submit();">
|
|
||||||
<?php foreach ($years as $y): ?>
|
|
||||||
<option value="<?= $y['id'] ?>" <?= $y['id'] == $year_id ? 'selected' : '' ?>>
|
|
||||||
<?= (int)$y['year'] ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Spaltengruppen berechnen -->
|
|
||||||
<?php
|
|
||||||
$columns = $summary['columns'];
|
|
||||||
$groups = [];
|
|
||||||
foreach ($columns as $col) {
|
|
||||||
$g = $col['group'];
|
|
||||||
if (!isset($groups[$g])) $groups[$g] = 0;
|
|
||||||
$groups[$g]++;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- Jahresübersicht Tabelle -->
|
|
||||||
<section>
|
|
||||||
<h2>Monatssummen <?= $current_year ? (int)$current_year['year'] : '' ?></h2>
|
|
||||||
<div class="journal-table-wrap">
|
|
||||||
<table class="journal-table">
|
|
||||||
<thead>
|
|
||||||
<tr class="journal-group-header">
|
|
||||||
<th colspan="2"></th>
|
|
||||||
<th colspan="2">Summen</th>
|
|
||||||
<?php foreach ($groups as $gname => $gcount): ?>
|
|
||||||
<th colspan="<?= $gcount ?>"><?= htmlspecialchars($gname) ?></th>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Monat</th>
|
|
||||||
<th class="journal-col-betrag">Betrag</th>
|
|
||||||
<th class="journal-soll">Soll</th>
|
|
||||||
<th class="journal-haben">Haben</th>
|
|
||||||
<?php foreach ($columns as $col): ?>
|
|
||||||
<th class="journal-col-amount journal-<?= $col['side'] ?>"><?= htmlspecialchars($col['label']) ?></th>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tr>
|
|
||||||
<!-- Jahressummen oben (wie Excel) -->
|
|
||||||
<tr class="journal-top-sums">
|
|
||||||
<td style="text-align:right;"><strong>S</strong></td>
|
|
||||||
<td class="journal-col-betrag"></td>
|
|
||||||
<td class="journal-col-amount journal-soll"><strong><?= journal_format_amount($summary['yearly_soll']) ?></strong></td>
|
|
||||||
<td class="journal-col-amount journal-haben"></td>
|
|
||||||
<?php foreach ($columns as $col): ?>
|
|
||||||
<td class="journal-col-amount journal-<?= $col['side'] ?>">
|
|
||||||
<?php if ($col['side'] === 'soll'): ?>
|
|
||||||
<strong><?= journal_format_amount($summary['yearly_totals'][$col['key']] ?? 0) ?></strong>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tr>
|
|
||||||
<tr class="journal-top-sums">
|
|
||||||
<td style="text-align:right;"><strong>H</strong></td>
|
|
||||||
<td class="journal-col-betrag"></td>
|
|
||||||
<td class="journal-col-amount journal-soll"></td>
|
|
||||||
<td class="journal-col-amount journal-haben"><strong><?= journal_format_amount($summary['yearly_haben']) ?></strong></td>
|
|
||||||
<?php foreach ($columns as $col): ?>
|
|
||||||
<td class="journal-col-amount journal-<?= $col['side'] ?>">
|
|
||||||
<?php if ($col['side'] === 'haben'): ?>
|
|
||||||
<strong><?= journal_format_amount($summary['yearly_totals'][$col['key']] ?? 0) ?></strong>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php for ($m = 1; $m <= 12; $m++): ?>
|
|
||||||
<?php $mt = $summary['months'][$m]; ?>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a href="<?= url_for('journal.php?year_id=' . $year_id . '&month=' . $m) ?>">
|
|
||||||
<?= journal_month_name_full($m) ?>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td class="journal-col-betrag"><?= journal_format_amount($mt['_amount'] ?? 0) ?></td>
|
|
||||||
<td class="journal-col-amount journal-soll"><?= journal_format_amount($mt['_soll'] ?? 0) ?></td>
|
|
||||||
<td class="journal-col-amount journal-haben"><?= journal_format_amount($mt['_haben'] ?? 0) ?></td>
|
|
||||||
<?php foreach ($columns as $col): ?>
|
|
||||||
<?php $col_val = $mt[$col['key']] ?? 0; ?>
|
|
||||||
<td class="journal-col-amount journal-<?= $col['side'] ?>">
|
|
||||||
<?php if ($col_val != 0): ?>
|
|
||||||
<a href="<?= url_for('journal_detail.php?year_id=' . $year_id . '&month=' . $m . '&col_key=' . urlencode($col['key'])) ?>" style="color:inherit;text-decoration:none;" title="Details anzeigen">
|
|
||||||
<?= journal_format_amount($col_val) ?>
|
|
||||||
</a>
|
|
||||||
<?php else: ?>
|
|
||||||
<?= journal_format_amount($col_val) ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tr>
|
|
||||||
<?php endfor; ?>
|
|
||||||
</tbody>
|
|
||||||
<tfoot>
|
|
||||||
<tr class="journal-totals-row">
|
|
||||||
<td><strong>Jahressumme</strong></td>
|
|
||||||
<td class="journal-col-betrag"><strong><?= journal_format_amount($summary['yearly_amount']) ?></strong></td>
|
|
||||||
<td class="journal-col-amount journal-soll"><strong><?= journal_format_amount($summary['yearly_soll']) ?></strong></td>
|
|
||||||
<td class="journal-col-amount journal-haben"><strong><?= journal_format_amount($summary['yearly_haben']) ?></strong></td>
|
|
||||||
<?php foreach ($columns as $col): ?>
|
|
||||||
<td class="journal-col-amount journal-<?= $col['side'] ?>">
|
|
||||||
<strong><?= journal_format_amount($summary['yearly_totals'][$col['key']] ?? 0) ?></strong>
|
|
||||||
</td>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Gewinnberechnung -->
|
|
||||||
<?php if ($profitability): ?>
|
|
||||||
<section>
|
|
||||||
<h2>Gewinnberechnung <?= $current_year ? (int)$current_year['year'] : '' ?></h2>
|
|
||||||
<div>
|
|
||||||
<table class="list">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Monat</th>
|
|
||||||
<th style="text-align:right;">Erlöse</th>
|
|
||||||
<th style="text-align:right;">Wareneingang</th>
|
|
||||||
<th style="text-align:right;">Aufwand</th>
|
|
||||||
<th style="text-align:right;">Gewinn</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php
|
|
||||||
$sum_er = 0; $sum_we = 0; $sum_au = 0; $sum_gw = 0;
|
|
||||||
for ($m = 1; $m <= 12; $m++):
|
|
||||||
$p = $profitability[$m];
|
|
||||||
$sum_er += $p['erloese'];
|
|
||||||
$sum_we += $p['wareneingang'];
|
|
||||||
$sum_au += $p['aufwand'];
|
|
||||||
$sum_gw += $p['gewinn'];
|
|
||||||
?>
|
|
||||||
<tr>
|
|
||||||
<td><?= journal_month_name_full($m) ?></td>
|
|
||||||
<td style="text-align:right;"><?= journal_format_amount($p['erloese']) ?></td>
|
|
||||||
<td style="text-align:right;"><?= journal_format_amount($p['wareneingang']) ?></td>
|
|
||||||
<td style="text-align:right;"><?= journal_format_amount($p['aufwand']) ?></td>
|
|
||||||
<td style="text-align:right;<?= $p['gewinn'] < 0 ? 'color:var(--error);' : '' ?>">
|
|
||||||
<strong><?= journal_format_amount($p['gewinn']) ?></strong>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endfor; ?>
|
|
||||||
</tbody>
|
|
||||||
<tfoot>
|
|
||||||
<tr>
|
|
||||||
<td><strong>Jahressumme</strong></td>
|
|
||||||
<td style="text-align:right;"><strong><?= journal_format_amount($sum_er) ?></strong></td>
|
|
||||||
<td style="text-align:right;"><strong><?= journal_format_amount($sum_we) ?></strong></td>
|
|
||||||
<td style="text-align:right;"><strong><?= journal_format_amount($sum_au) ?></strong></td>
|
|
||||||
<td style="text-align:right;<?= $sum_gw < 0 ? 'color:var(--error);' : '' ?>">
|
|
||||||
<strong><?= journal_format_amount($sum_gw) ?></strong>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php endif; ?>
|
|
||||||
</main>
|
|
||||||
<script src="assets/command-palette.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
|
|
||||||
$error = '';
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$user = $_POST['username'] ?? '';
|
|
||||||
$pass = $_POST['password'] ?? '';
|
|
||||||
if (login($user, $pass)) {
|
|
||||||
header('Location: ' . url_for('index.php'));
|
|
||||||
exit;
|
|
||||||
} else {
|
|
||||||
$error = 'Login fehlgeschlagen.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>PIRP Login</title>
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="login-wrap">
|
|
||||||
<div class="login-box">
|
|
||||||
<h1>PIRP</h1>
|
|
||||||
<?php if ($error): ?><p class="error"><?= htmlspecialchars($error) ?></p><?php endif; ?>
|
|
||||||
<form method="post">
|
|
||||||
<label>Benutzer:
|
|
||||||
<input type="text" name="username" required autofocus>
|
|
||||||
</label>
|
|
||||||
<label>Passwort:
|
|
||||||
<input type="password" name="password" required>
|
|
||||||
</label>
|
|
||||||
<button type="submit" style="width:100%;margin-top:4px;">Login</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
logout();
|
|
||||||
header('Location: ' . url_for('login.php'));
|
|
||||||
exit;
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_once __DIR__ . '/../src/invoice_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/pdf_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/icons.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
$invoice_id = isset($_GET['invoice_id']) ? (int)$_GET['invoice_id'] : 0;
|
|
||||||
if (!$invoice_id) {
|
|
||||||
header('Location: ' . url_for('invoices.php'));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$inv = get_invoice_with_customer($invoice_id);
|
|
||||||
if (!$inv || $inv['paid']) {
|
|
||||||
header('Location: ' . url_for('invoices.php?msg=' . urlencode('Mahnung nur für offene Rechnungen möglich.')));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo = get_db();
|
|
||||||
|
|
||||||
// Mahnstufe auto-bestimmen
|
|
||||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM mahnungen WHERE invoice_id = :id");
|
|
||||||
$stmt->execute([':id' => $invoice_id]);
|
|
||||||
$existing_count = (int)$stmt->fetchColumn();
|
|
||||||
$next_level = min($existing_count + 1, 3);
|
|
||||||
|
|
||||||
$error = '';
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$mahnung_date = $_POST['mahnung_date'] ?? date('Y-m-d');
|
|
||||||
$level = (int)($_POST['level'] ?? $next_level);
|
|
||||||
$fee_amount = (float)str_replace(',', '.', $_POST['fee_amount'] ?? '0');
|
|
||||||
|
|
||||||
if ($level < 1 || $level > 3) $level = $next_level;
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO mahnungen (invoice_id, mahnung_date, level, fee_amount)
|
|
||||||
VALUES (:iid, :mdate, :level, :fee)
|
|
||||||
RETURNING id");
|
|
||||||
$stmt->execute([
|
|
||||||
':iid' => $invoice_id,
|
|
||||||
':mdate' => $mahnung_date,
|
|
||||||
':level' => $level,
|
|
||||||
':fee' => $fee_amount,
|
|
||||||
]);
|
|
||||||
$mahnung_id = (int)$stmt->fetchColumn();
|
|
||||||
|
|
||||||
archive_mahnung_pdf($mahnung_id);
|
|
||||||
|
|
||||||
header('Location: ' . url_for('mahnung_pdf.php?id=' . $mahnung_id));
|
|
||||||
exit;
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$error = $e->getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$level_labels = [1 => 'Mahnung', 2 => '2. Mahnung', 3 => '3. Mahnung (Letzte)'];
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Mahnung – <?= htmlspecialchars($inv['invoice_number']) ?></title>
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>PIRP</h1>
|
|
||||||
<nav>
|
|
||||||
<a href="<?= url_for('index.php') ?>"><?= icon_dashboard() ?>Dashboard</a>
|
|
||||||
<a href="<?= url_for('invoices.php') ?>" class="active"><?= icon_invoices() ?>Rechnungen</a>
|
|
||||||
<a href="<?= url_for('customers.php') ?>"><?= icon_customers() ?>Kunden</a>
|
|
||||||
<a href="<?= url_for('expenses.php') ?>"><?= icon_expenses() ?>Ausgaben</a>
|
|
||||||
<a href="<?= url_for('belegarchiv.php') ?>"><?= icon_archive() ?>Belege</a>
|
|
||||||
<a href="<?= url_for('journal.php') ?>"><?= icon_journal() ?>Journal</a>
|
|
||||||
<a href="<?= url_for('euer.php') ?>"><?= icon_euer() ?>EÜR</a>
|
|
||||||
<a href="<?= url_for('settings.php') ?>"><?= icon_settings() ?>Einstellungen</a>
|
|
||||||
<a href="<?= url_for('logout.php') ?>"><?= icon_logout() ?>Logout (<?= htmlspecialchars($_SESSION['username'] ?? '') ?>)</a>
|
|
||||||
<span class="cmd-k-hint" onclick="document.dispatchEvent(new KeyboardEvent('keydown',{key:'k',ctrlKey:true}))"><kbd>Ctrl+K</kbd></span>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<div class="module-subnav">
|
|
||||||
<a href="<?= url_for('invoices.php') ?>">← Zurück zu Rechnungen</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($error): ?>
|
|
||||||
<p class="error"><?= htmlspecialchars($error) ?></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>Mahnung erstellen</h2>
|
|
||||||
<div style="max-width:460px;">
|
|
||||||
<table class="list" style="margin-bottom:16px;">
|
|
||||||
<tr><td style="color:var(--text-muted);width:140px;">Rechnung</td><td><strong><?= htmlspecialchars($inv['invoice_number']) ?></strong></td></tr>
|
|
||||||
<tr><td style="color:var(--text-muted);">Kunde</td><td><?= htmlspecialchars($inv['customer_name']) ?></td></tr>
|
|
||||||
<tr><td style="color:var(--text-muted);">Rechnungsdatum</td><td><?= date('d.m.Y', strtotime($inv['invoice_date'])) ?></td></tr>
|
|
||||||
<tr><td style="color:var(--text-muted);">Betrag</td><td><?= number_format($inv['total_gross'], 2, ',', '.') ?> €</td></tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<form method="post" style="display:flex;flex-direction:column;gap:12px;">
|
|
||||||
<label>Mahnstufe:
|
|
||||||
<select name="level">
|
|
||||||
<?php foreach ($level_labels as $l => $lbl): ?>
|
|
||||||
<option value="<?= $l ?>" <?= $l == $next_level ? 'selected' : '' ?>><?= $lbl ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>Mahndatum:
|
|
||||||
<input type="date" name="mahnung_date" value="<?= date('Y-m-d') ?>" required>
|
|
||||||
</label>
|
|
||||||
<label>Mahngebühr (€):
|
|
||||||
<input type="text" name="fee_amount" value="0,00" style="width:120px;">
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
<button type="submit">Mahnung erstellen & PDF öffnen</button>
|
|
||||||
<a href="<?= url_for('invoices.php') ?>" style="margin-left:12px;">Abbrechen</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<script src="assets/command-palette.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_once __DIR__ . '/../src/invoice_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/pdf_functions.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
|
||||||
if (!$id) {
|
|
||||||
header('Location: ' . url_for('invoices.php'));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->prepare("SELECT m.*, i.invoice_number FROM mahnungen m JOIN invoices i ON i.id = m.invoice_id WHERE m.id = :id");
|
|
||||||
$stmt->execute([':id' => $id]);
|
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
if (!$row) {
|
|
||||||
header('Location: ' . url_for('invoices.php'));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// PDF vorhanden?
|
|
||||||
if (empty($row['pdf_path'])) {
|
|
||||||
archive_mahnung_pdf($id);
|
|
||||||
// Neu laden
|
|
||||||
$stmt->execute([':id' => $id]);
|
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
}
|
|
||||||
|
|
||||||
$fullPath = __DIR__ . '/' . ($row['pdf_path'] ?? '');
|
|
||||||
if (!file_exists($fullPath)) {
|
|
||||||
http_response_code(404);
|
|
||||||
echo 'PDF nicht gefunden.';
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$safe_name = 'MAHNUNG-' . preg_replace('/[^A-Za-z0-9\-]/', '_', $row['invoice_number']) . '-L' . $row['level'] . '.pdf';
|
|
||||||
header('Content-Type: application/pdf');
|
|
||||||
header('Content-Disposition: inline; filename="' . $safe_name . '"');
|
|
||||||
header('Content-Length: ' . filesize($fullPath));
|
|
||||||
readfile($fullPath);
|
|
||||||
exit;
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_once __DIR__ . '/../src/recurring_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/icons.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
$msg = '';
|
|
||||||
$error = '';
|
|
||||||
|
|
||||||
$action = $_GET['action'] ?? '';
|
|
||||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
|
||||||
|
|
||||||
// Aktionen verarbeiten
|
|
||||||
if ($action === 'delete' && $id) {
|
|
||||||
delete_recurring_template($id);
|
|
||||||
$msg = 'Abo-Vorlage gelöscht.';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($action === 'toggle' && $id) {
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->prepare("UPDATE recurring_templates SET is_active = NOT is_active WHERE id = :id");
|
|
||||||
$stmt->execute([':id' => $id]);
|
|
||||||
$msg = 'Status geändert.';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter
|
|
||||||
$show_all = isset($_GET['show_all']);
|
|
||||||
$templates = get_recurring_templates(!$show_all);
|
|
||||||
$pending_count = count_pending_recurring_invoices();
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Abo-Rechnungen</title>
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>PIRP</h1>
|
|
||||||
<nav>
|
|
||||||
<a href="<?= url_for('index.php') ?>"><?= icon_dashboard() ?>Dashboard</a>
|
|
||||||
<a href="<?= url_for('invoices.php') ?>" class="active"><?= icon_invoices() ?>Rechnungen</a>
|
|
||||||
<a href="<?= url_for('customers.php') ?>"><?= icon_customers() ?>Kunden</a>
|
|
||||||
<a href="<?= url_for('expenses.php') ?>"><?= icon_expenses() ?>Ausgaben</a>
|
|
||||||
<a href="<?= url_for('belegarchiv.php') ?>"><?= icon_archive() ?>Belege</a>
|
|
||||||
<a href="<?= url_for('journal.php') ?>"><?= icon_journal() ?>Journal</a>
|
|
||||||
<a href="<?= url_for('euer.php') ?>"><?= icon_euer() ?>EÜR</a>
|
|
||||||
<a href="<?= url_for('settings.php') ?>"><?= icon_settings() ?>Einstellungen</a>
|
|
||||||
<a href="<?= url_for('logout.php') ?>"><?= icon_logout() ?>Logout (<?= htmlspecialchars($_SESSION['username'] ?? '') ?>)</a>
|
|
||||||
<span class="cmd-k-hint" onclick="document.dispatchEvent(new KeyboardEvent('keydown',{key:'k',ctrlKey:true}))"><kbd>Ctrl+K</kbd></span>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<div class="module-subnav">
|
|
||||||
<a href="<?= url_for('invoices.php') ?>">Übersicht</a>
|
|
||||||
<a href="<?= url_for('invoice_new.php') ?>">Neue Rechnung</a>
|
|
||||||
<a href="<?= url_for('recurring.php') ?>" class="active">Abo-Rechnungen</a>
|
|
||||||
</div>
|
|
||||||
<?php if ($msg): ?><p class="success"><?= htmlspecialchars($msg) ?></p><?php endif; ?>
|
|
||||||
<?php if ($error): ?><p class="error"><?= htmlspecialchars($error) ?></p><?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($pending_count > 0): ?>
|
|
||||||
<div class="warning">
|
|
||||||
<strong><?= $pending_count ?> Abo-Rechnung<?= $pending_count > 1 ? 'en' : '' ?> fällig!</strong>
|
|
||||||
<a href="<?= url_for('recurring_generate.php') ?>" class="button" style="margin-left:10px;">Jetzt generieren</a>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>Abo-Vorlagen</h2>
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
<a href="<?= url_for('recurring_edit.php') ?>" class="button">Neue Abo-Vorlage</a>
|
|
||||||
<a href="<?= url_for('recurring_generate.php') ?>" class="button-secondary">Rechnungen generieren</a>
|
|
||||||
<?php if ($show_all): ?>
|
|
||||||
<a href="<?= url_for('recurring.php') ?>" class="button-secondary">Nur aktive anzeigen</a>
|
|
||||||
<?php else: ?>
|
|
||||||
<a href="<?= url_for('recurring.php?show_all=1') ?>" class="button-secondary">Alle anzeigen</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<table class="list">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Kunde</th>
|
|
||||||
<th>Intervall</th>
|
|
||||||
<th>Nächste Fälligkeit</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Aktionen</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($templates as $t): ?>
|
|
||||||
<?php
|
|
||||||
$is_due = $t['is_active'] && strtotime($t['next_due_date']) <= time();
|
|
||||||
?>
|
|
||||||
<tr>
|
|
||||||
<td><?= htmlspecialchars($t['template_name']) ?></td>
|
|
||||||
<td>
|
|
||||||
<?= htmlspecialchars($t['customer_name']) ?>
|
|
||||||
<?php if ($t['customer_number']): ?>
|
|
||||||
<br><small><?= htmlspecialchars($t['customer_number']) ?></small>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="interval-badge interval-<?= $t['interval_type'] ?>">
|
|
||||||
<?= get_interval_label($t['interval_type']) ?>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<?= date('d.m.Y', strtotime($t['next_due_date'])) ?>
|
|
||||||
<?php if ($is_due): ?>
|
|
||||||
<span class="badge badge-warning">Fällig</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<?php if ($t['is_active']): ?>
|
|
||||||
<span class="badge badge-success">Aktiv</span>
|
|
||||||
<?php else: ?>
|
|
||||||
<span class="badge badge-danger">Inaktiv</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="<?= url_for('recurring_edit.php?id=' . $t['id']) ?>">Bearbeiten</a>
|
|
||||||
<a href="<?= url_for('recurring.php?action=toggle&id=' . $t['id']) ?>">
|
|
||||||
<?= $t['is_active'] ? 'Deaktivieren' : 'Aktivieren' ?>
|
|
||||||
</a>
|
|
||||||
<a href="<?= url_for('recurring.php?action=delete&id=' . $t['id']) ?>"
|
|
||||||
onclick="return confirm('Abo-Vorlage wirklich löschen?');">Löschen</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php if (empty($templates)): ?>
|
|
||||||
<tr><td colspan="6">Keine Abo-Vorlagen gefunden.</td></tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<script src="assets/command-palette.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_once __DIR__ . '/../src/invoice_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/customer_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/recurring_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/icons.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
$settings = get_settings();
|
|
||||||
$customers = get_customers();
|
|
||||||
$error = '';
|
|
||||||
$msg = '';
|
|
||||||
|
|
||||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
|
||||||
$template = null;
|
|
||||||
$items = [];
|
|
||||||
|
|
||||||
if ($id) {
|
|
||||||
$template = get_recurring_template($id);
|
|
||||||
if (!$template) {
|
|
||||||
header('Location: ' . url_for('recurring.php'));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
$items = get_recurring_template_items($id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$data = [
|
|
||||||
'template_name' => trim($_POST['template_name'] ?? ''),
|
|
||||||
'customer_id' => (int)($_POST['customer_id'] ?? 0),
|
|
||||||
'interval_type' => $_POST['interval_type'] ?? 'monthly',
|
|
||||||
'start_date' => $_POST['start_date'] ?? date('Y-m-d'),
|
|
||||||
'end_date' => $_POST['end_date'] ?: null,
|
|
||||||
'next_due_date' => $_POST['next_due_date'] ?? date('Y-m-d'),
|
|
||||||
'vat_mode' => $settings['vat_mode'] ?? 'klein',
|
|
||||||
'vat_rate' => (float)($settings['default_vat_rate'] ?? 19.0),
|
|
||||||
'is_active' => !empty($_POST['is_active']),
|
|
||||||
'notes_internal' => $_POST['notes_internal'] ?? ''
|
|
||||||
];
|
|
||||||
|
|
||||||
// Validierung
|
|
||||||
if (empty($data['template_name'])) {
|
|
||||||
$error = 'Bitte einen Namen eingeben.';
|
|
||||||
} elseif ($data['customer_id'] <= 0) {
|
|
||||||
$error = 'Bitte einen Kunden auswählen.';
|
|
||||||
} else {
|
|
||||||
// Positionen sammeln
|
|
||||||
$newItems = [];
|
|
||||||
$count = isset($_POST['item_desc']) ? count($_POST['item_desc']) : 0;
|
|
||||||
|
|
||||||
for ($i = 0; $i < $count; $i++) {
|
|
||||||
$desc = trim($_POST['item_desc'][$i] ?? '');
|
|
||||||
$qty = (float)($_POST['item_qty'][$i] ?? 0);
|
|
||||||
$price = (float)($_POST['item_price'][$i] ?? 0);
|
|
||||||
|
|
||||||
if ($desc !== '' && $qty > 0) {
|
|
||||||
$newItems[] = [
|
|
||||||
'position_no' => count($newItems) + 1,
|
|
||||||
'description' => $desc,
|
|
||||||
'quantity' => $qty,
|
|
||||||
'unit_price' => $price
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($newItems)) {
|
|
||||||
$error = 'Bitte mindestens eine Position ausfüllen.';
|
|
||||||
} else {
|
|
||||||
$template_id = save_recurring_template($id, $data);
|
|
||||||
save_recurring_template_items($template_id, $newItems);
|
|
||||||
|
|
||||||
header('Location: ' . url_for('recurring.php'));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title><?= $id ? 'Abo-Vorlage bearbeiten' : 'Neue Abo-Vorlage' ?></title>
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
<script>
|
|
||||||
function addRow() {
|
|
||||||
const tbody = document.getElementById('items-body');
|
|
||||||
const index = tbody.children.length;
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
tr.innerHTML = `
|
|
||||||
<td>${index+1}</td>
|
|
||||||
<td><input type="text" name="item_desc[${index}]" size="40"></td>
|
|
||||||
<td><input type="number" step="0.01" name="item_qty[${index}]" value="1"></td>
|
|
||||||
<td><input type="number" step="0.01" name="item_price[${index}]" value="0.00"></td>
|
|
||||||
<td><button type="button" onclick="this.closest('tr').remove()">X</button></td>
|
|
||||||
`;
|
|
||||||
tbody.appendChild(tr);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>PIRP</h1>
|
|
||||||
<nav>
|
|
||||||
<a href="<?= url_for('index.php') ?>"><?= icon_dashboard() ?>Dashboard</a>
|
|
||||||
<a href="<?= url_for('invoices.php') ?>" class="active"><?= icon_invoices() ?>Rechnungen</a>
|
|
||||||
<a href="<?= url_for('customers.php') ?>"><?= icon_customers() ?>Kunden</a>
|
|
||||||
<a href="<?= url_for('expenses.php') ?>"><?= icon_expenses() ?>Ausgaben</a>
|
|
||||||
<a href="<?= url_for('belegarchiv.php') ?>"><?= icon_archive() ?>Belege</a>
|
|
||||||
<a href="<?= url_for('journal.php') ?>"><?= icon_journal() ?>Journal</a>
|
|
||||||
<a href="<?= url_for('euer.php') ?>"><?= icon_euer() ?>EÜR</a>
|
|
||||||
<a href="<?= url_for('settings.php') ?>"><?= icon_settings() ?>Einstellungen</a>
|
|
||||||
<a href="<?= url_for('logout.php') ?>"><?= icon_logout() ?>Logout (<?= htmlspecialchars($_SESSION['username'] ?? '') ?>)</a>
|
|
||||||
<span class="cmd-k-hint" onclick="document.dispatchEvent(new KeyboardEvent('keydown',{key:'k',ctrlKey:true}))"><kbd>Ctrl+K</kbd></span>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<div class="module-subnav">
|
|
||||||
<a href="<?= url_for('invoices.php') ?>">Übersicht</a>
|
|
||||||
<a href="<?= url_for('invoice_new.php') ?>">Neue Rechnung</a>
|
|
||||||
<a href="<?= url_for('recurring.php') ?>" class="active">Abo-Rechnungen</a>
|
|
||||||
</div>
|
|
||||||
<?php if ($error): ?><p class="error"><?= htmlspecialchars($error) ?></p><?php endif; ?>
|
|
||||||
|
|
||||||
<form method="post">
|
|
||||||
<section>
|
|
||||||
<h2>Abo-Details</h2>
|
|
||||||
<div>
|
|
||||||
<label>Name der Vorlage:
|
|
||||||
<input type="text" name="template_name"
|
|
||||||
value="<?= htmlspecialchars($template['template_name'] ?? '') ?>" required>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>Kunde:
|
|
||||||
<select name="customer_id" required>
|
|
||||||
<option value="">-- wählen --</option>
|
|
||||||
<?php foreach ($customers as $c): ?>
|
|
||||||
<option value="<?= $c['id'] ?>"
|
|
||||||
<?= ($template['customer_id'] ?? 0) == $c['id'] ? 'selected' : '' ?>>
|
|
||||||
<?= htmlspecialchars($c['name']) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>Intervall:
|
|
||||||
<select name="interval_type" required>
|
|
||||||
<option value="monthly" <?= ($template['interval_type'] ?? '') === 'monthly' ? 'selected' : '' ?>>
|
|
||||||
Monatlich
|
|
||||||
</option>
|
|
||||||
<option value="quarterly" <?= ($template['interval_type'] ?? '') === 'quarterly' ? 'selected' : '' ?>>
|
|
||||||
Quartalsweise (alle 3 Monate)
|
|
||||||
</option>
|
|
||||||
<option value="yearly" <?= ($template['interval_type'] ?? '') === 'yearly' ? 'selected' : '' ?>>
|
|
||||||
Jährlich
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="flex-row">
|
|
||||||
<label>Startdatum:
|
|
||||||
<input type="date" name="start_date"
|
|
||||||
value="<?= htmlspecialchars($template['start_date'] ?? date('Y-m-d')) ?>" required>
|
|
||||||
</label>
|
|
||||||
<label>Enddatum (optional):
|
|
||||||
<input type="date" name="end_date"
|
|
||||||
value="<?= htmlspecialchars($template['end_date'] ?? '') ?>">
|
|
||||||
</label>
|
|
||||||
<label>Nächste Fälligkeit:
|
|
||||||
<input type="date" name="next_due_date"
|
|
||||||
value="<?= htmlspecialchars($template['next_due_date'] ?? date('Y-m-d')) ?>" required>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" name="is_active" value="1"
|
|
||||||
<?= ($template['is_active'] ?? true) ? 'checked' : '' ?>>
|
|
||||||
Aktiv (Rechnungen werden generiert)
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>Interne Notizen:
|
|
||||||
<textarea name="notes_internal" rows="2"><?= htmlspecialchars($template['notes_internal'] ?? '') ?></textarea>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>Positionen</h2>
|
|
||||||
<div>
|
|
||||||
<table class="list" id="items-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style="width:5%">Pos.</th>
|
|
||||||
<th>Beschreibung</th>
|
|
||||||
<th style="width:12%">Menge</th>
|
|
||||||
<th style="width:15%">Einzelpreis (netto)</th>
|
|
||||||
<th style="width:5%"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="items-body">
|
|
||||||
<?php
|
|
||||||
// Bestehende Positionen oder leere Zeilen
|
|
||||||
$displayItems = !empty($items) ? $items : [
|
|
||||||
['description' => '', 'quantity' => 1, 'unit_price' => 0],
|
|
||||||
['description' => '', 'quantity' => 1, 'unit_price' => 0],
|
|
||||||
['description' => '', 'quantity' => 1, 'unit_price' => 0]
|
|
||||||
];
|
|
||||||
foreach ($displayItems as $i => $item):
|
|
||||||
?>
|
|
||||||
<tr>
|
|
||||||
<td><?= $i + 1 ?></td>
|
|
||||||
<td><input type="text" name="item_desc[<?= $i ?>]"
|
|
||||||
value="<?= htmlspecialchars($item['description']) ?>" size="40"></td>
|
|
||||||
<td><input type="number" step="0.01" name="item_qty[<?= $i ?>]"
|
|
||||||
value="<?= number_format($item['quantity'], 2, '.', '') ?>"></td>
|
|
||||||
<td><input type="number" step="0.01" name="item_price[<?= $i ?>]"
|
|
||||||
value="<?= number_format($item['unit_price'], 2, '.', '') ?>"></td>
|
|
||||||
<td><button type="button" onclick="this.closest('tr').remove()">X</button></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p>
|
|
||||||
<button type="button" onclick="addRow()" class="button-secondary">Position hinzufügen</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<button type="submit">Speichern</button>
|
|
||||||
<a href="<?= url_for('recurring.php') ?>" class="button-secondary">Abbrechen</a>
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<?php if ($id): ?>
|
|
||||||
<section>
|
|
||||||
<h2>Generierte Rechnungen</h2>
|
|
||||||
<div>
|
|
||||||
<?php $log = get_recurring_log($id); ?>
|
|
||||||
<?php if (empty($log)): ?>
|
|
||||||
<p>Noch keine Rechnungen generiert.</p>
|
|
||||||
<?php else: ?>
|
|
||||||
<table class="list">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>Fälligkeit</th>
|
|
||||||
<th>Rechnungsnr.</th>
|
|
||||||
<th>Betrag</th>
|
|
||||||
<th>Aktion</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($log as $entry): ?>
|
|
||||||
<tr>
|
|
||||||
<td><?= date('d.m.Y H:i', strtotime($entry['generated_at'])) ?></td>
|
|
||||||
<td><?= date('d.m.Y', strtotime($entry['due_date'])) ?></td>
|
|
||||||
<td><?= htmlspecialchars($entry['invoice_number'] ?? '-') ?></td>
|
|
||||||
<td><?= $entry['total_gross'] ? number_format($entry['total_gross'], 2, ',', '.') . ' €' : '-' ?></td>
|
|
||||||
<td>
|
|
||||||
<?php if ($entry['invoice_id']): ?>
|
|
||||||
<a href="<?= url_for('invoice_pdf.php?id=' . $entry['invoice_id']) ?>" target="_blank">PDF</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
|
||||||
</main>
|
|
||||||
<script src="assets/command-palette.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_once __DIR__ . '/../src/recurring_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/icons.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
$msg = '';
|
|
||||||
$error = '';
|
|
||||||
$generated = [];
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$selected = $_POST['template_ids'] ?? [];
|
|
||||||
|
|
||||||
if (empty($selected)) {
|
|
||||||
$error = 'Bitte mindestens eine Vorlage auswählen.';
|
|
||||||
} else {
|
|
||||||
$success = 0;
|
|
||||||
$failed = 0;
|
|
||||||
|
|
||||||
foreach ($selected as $tid) {
|
|
||||||
$tid = (int)$tid;
|
|
||||||
$invoice_id = generate_invoice_from_template($tid);
|
|
||||||
|
|
||||||
if ($invoice_id) {
|
|
||||||
$success++;
|
|
||||||
$generated[] = $invoice_id;
|
|
||||||
} else {
|
|
||||||
$failed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($success > 0) {
|
|
||||||
$msg = "$success Rechnung(en) erfolgreich generiert.";
|
|
||||||
}
|
|
||||||
if ($failed > 0) {
|
|
||||||
$error = "$failed Rechnung(en) konnten nicht generiert werden.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$pending = get_pending_recurring_invoices();
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Abo-Rechnungen generieren</title>
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
<script>
|
|
||||||
function toggleAll(checkbox) {
|
|
||||||
document.querySelectorAll('input[name="template_ids[]"]').forEach(cb => {
|
|
||||||
cb.checked = checkbox.checked;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>PIRP</h1>
|
|
||||||
<nav>
|
|
||||||
<a href="<?= url_for('index.php') ?>"><?= icon_dashboard() ?>Dashboard</a>
|
|
||||||
<a href="<?= url_for('invoices.php') ?>" class="active"><?= icon_invoices() ?>Rechnungen</a>
|
|
||||||
<a href="<?= url_for('customers.php') ?>"><?= icon_customers() ?>Kunden</a>
|
|
||||||
<a href="<?= url_for('expenses.php') ?>"><?= icon_expenses() ?>Ausgaben</a>
|
|
||||||
<a href="<?= url_for('belegarchiv.php') ?>"><?= icon_archive() ?>Belege</a>
|
|
||||||
<a href="<?= url_for('journal.php') ?>"><?= icon_journal() ?>Journal</a>
|
|
||||||
<a href="<?= url_for('euer.php') ?>"><?= icon_euer() ?>EÜR</a>
|
|
||||||
<a href="<?= url_for('settings.php') ?>"><?= icon_settings() ?>Einstellungen</a>
|
|
||||||
<a href="<?= url_for('logout.php') ?>"><?= icon_logout() ?>Logout (<?= htmlspecialchars($_SESSION['username'] ?? '') ?>)</a>
|
|
||||||
<span class="cmd-k-hint" onclick="document.dispatchEvent(new KeyboardEvent('keydown',{key:'k',ctrlKey:true}))"><kbd>Ctrl+K</kbd></span>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<div class="module-subnav">
|
|
||||||
<a href="<?= url_for('invoices.php') ?>">Übersicht</a>
|
|
||||||
<a href="<?= url_for('invoice_new.php') ?>">Neue Rechnung</a>
|
|
||||||
<a href="<?= url_for('recurring.php') ?>" class="active">Abo-Rechnungen</a>
|
|
||||||
</div>
|
|
||||||
<?php if ($msg): ?><p class="success"><?= htmlspecialchars($msg) ?></p><?php endif; ?>
|
|
||||||
<?php if ($error): ?><p class="error"><?= htmlspecialchars($error) ?></p><?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (!empty($generated)): ?>
|
|
||||||
<section>
|
|
||||||
<h2>Generierte Rechnungen</h2>
|
|
||||||
<div>
|
|
||||||
<ul>
|
|
||||||
<?php foreach ($generated as $inv_id): ?>
|
|
||||||
<li><a href="<?= url_for('invoice_pdf.php?id=' . $inv_id) ?>" target="_blank">Rechnung #<?= $inv_id ?> anzeigen</a></li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>Fällige Abo-Rechnungen</h2>
|
|
||||||
<div>
|
|
||||||
<?php if (empty($pending)): ?>
|
|
||||||
<p class="success">Keine fälligen Abo-Rechnungen. Alles erledigt!</p>
|
|
||||||
<p><a href="<?= url_for('recurring.php') ?>" class="button-secondary">Zurück zur Übersicht</a></p>
|
|
||||||
<?php else: ?>
|
|
||||||
<form method="post">
|
|
||||||
<table class="list">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style="width:30px"><input type="checkbox" onclick="toggleAll(this)" checked></th>
|
|
||||||
<th>Vorlage</th>
|
|
||||||
<th>Kunde</th>
|
|
||||||
<th>Intervall</th>
|
|
||||||
<th>Fällig seit</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($pending as $p): ?>
|
|
||||||
<?php
|
|
||||||
$items = get_recurring_template_items($p['id']);
|
|
||||||
$total = 0;
|
|
||||||
foreach ($items as $item) {
|
|
||||||
$total += $item['quantity'] * $item['unit_price'];
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<tr>
|
|
||||||
<td><input type="checkbox" name="template_ids[]" value="<?= $p['id'] ?>" checked></td>
|
|
||||||
<td>
|
|
||||||
<?= htmlspecialchars($p['template_name']) ?>
|
|
||||||
<br><small>ca. <?= number_format($total, 2, ',', '.') ?> € netto</small>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<?= htmlspecialchars($p['customer_name']) ?>
|
|
||||||
<?php if ($p['customer_number']): ?>
|
|
||||||
<br><small><?= htmlspecialchars($p['customer_number']) ?></small>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="interval-badge interval-<?= $p['interval_type'] ?>">
|
|
||||||
<?= get_interval_label($p['interval_type']) ?>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td><?= date('d.m.Y', strtotime($p['next_due_date'])) ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<p style="margin-top:15px">
|
|
||||||
<button type="submit">Ausgewählte Rechnungen generieren</button>
|
|
||||||
<a href="<?= url_for('recurring.php') ?>" class="button-secondary">Abbrechen</a>
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<script src="assets/command-palette.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
|
|
||||||
$q = trim($_GET['q'] ?? '');
|
|
||||||
if (mb_strlen($q) < 2) {
|
|
||||||
echo json_encode(['ok' => true, 'results' => []]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo = get_db();
|
|
||||||
$like = '%' . $q . '%';
|
|
||||||
$results = [];
|
|
||||||
|
|
||||||
// Rechnungen
|
|
||||||
$stmt = $pdo->prepare("SELECT i.id, i.invoice_number, i.total_gross, c.name AS customer_name
|
|
||||||
FROM invoices i
|
|
||||||
JOIN customers c ON c.id = i.customer_id
|
|
||||||
WHERE i.invoice_number ILIKE :q OR c.name ILIKE :q2
|
|
||||||
ORDER BY i.created_at DESC
|
|
||||||
LIMIT 5");
|
|
||||||
$stmt->execute([':q' => $like, ':q2' => $like]);
|
|
||||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|
||||||
$results[] = [
|
|
||||||
'type' => 'invoice',
|
|
||||||
'title' => $row['invoice_number'],
|
|
||||||
'subtitle' => $row['customer_name'] . ' - ' . number_format((float)$row['total_gross'], 2, ',', '.') . ' €',
|
|
||||||
'url' => url_for('invoice_pdf.php?id=' . $row['id']),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kunden
|
|
||||||
$stmt = $pdo->prepare("SELECT id, name, city FROM customers
|
|
||||||
WHERE name ILIKE :q OR city ILIKE :q2
|
|
||||||
ORDER BY name
|
|
||||||
LIMIT 5");
|
|
||||||
$stmt->execute([':q' => $like, ':q2' => $like]);
|
|
||||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|
||||||
$results[] = [
|
|
||||||
'type' => 'customer',
|
|
||||||
'title' => $row['name'],
|
|
||||||
'subtitle' => $row['city'] ?? '',
|
|
||||||
'url' => url_for('customers.php?action=edit&id=' . $row['id']),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ausgaben
|
|
||||||
$stmt = $pdo->prepare("SELECT id, description, amount, category FROM expenses
|
|
||||||
WHERE description ILIKE :q OR category ILIKE :q2
|
|
||||||
ORDER BY expense_date DESC
|
|
||||||
LIMIT 5");
|
|
||||||
$stmt->execute([':q' => $like, ':q2' => $like]);
|
|
||||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|
||||||
$results[] = [
|
|
||||||
'type' => 'expense',
|
|
||||||
'title' => $row['description'],
|
|
||||||
'subtitle' => ($row['category'] ?? '') . ' - ' . number_format((float)$row['amount'], 2, ',', '.') . ' €',
|
|
||||||
'url' => url_for('expenses.php?action=edit&id=' . $row['id']),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Journal-Einträge
|
|
||||||
$stmt = $pdo->prepare("SELECT id, description, amount, entry_date FROM journal_entries
|
|
||||||
WHERE description ILIKE :q
|
|
||||||
ORDER BY entry_date DESC
|
|
||||||
LIMIT 5");
|
|
||||||
$stmt->execute([':q' => $like]);
|
|
||||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|
||||||
$results[] = [
|
|
||||||
'type' => 'journal',
|
|
||||||
'title' => $row['description'],
|
|
||||||
'subtitle' => date('d.m.Y', strtotime($row['entry_date'])) . ' - ' . number_format((float)$row['amount'], 2, ',', '.') . ' €',
|
|
||||||
'url' => url_for('journal_entry.php?id=' . $row['id']),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
echo json_encode(['ok' => true, 'results' => $results]);
|
|
||||||
@@ -1,802 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/auth.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_once __DIR__ . '/../src/invoice_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/journal_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/icons.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
$settings = get_settings();
|
|
||||||
$msg = '';
|
|
||||||
$error = '';
|
|
||||||
|
|
||||||
// Aktiver Tab
|
|
||||||
$tab = $_GET['tab'] ?? 'allgemein';
|
|
||||||
$journal_sub = $_GET['jsub'] ?? 'jahre';
|
|
||||||
|
|
||||||
// ---- POST-Aktionen ----
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$form = $_POST['form'] ?? '';
|
|
||||||
|
|
||||||
// === ALLGEMEIN TAB ===
|
|
||||||
if ($form === 'allgemein') {
|
|
||||||
$data = [
|
|
||||||
'company_name' => $_POST['company_name'] ?? '',
|
|
||||||
'company_address' => $_POST['company_address'] ?? '',
|
|
||||||
'company_zip' => $_POST['company_zip'] ?? '',
|
|
||||||
'company_city' => $_POST['company_city'] ?? '',
|
|
||||||
'company_country' => $_POST['company_country'] ?? '',
|
|
||||||
'tax_id' => $_POST['tax_id'] ?? '',
|
|
||||||
'vat_mode' => $_POST['vat_mode'] ?? 'klein',
|
|
||||||
'default_vat_rate'=> $_POST['default_vat_rate'] ?? 19.0,
|
|
||||||
'payment_terms' => $_POST['payment_terms'] ?? '',
|
|
||||||
'footer_text' => $_POST['footer_text'] ?? '',
|
|
||||||
'logo_path' => $settings['logo_path'] ?? null,
|
|
||||||
'iban' => $_POST['iban'] ?? '',
|
|
||||||
'phone' => $_POST['phone'] ?? '',
|
|
||||||
'email' => $_POST['email'] ?? '',
|
|
||||||
'website' => $_POST['website'] ?? '',
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!empty($_FILES['logo']['tmp_name'])) {
|
|
||||||
$targetDir = __DIR__ . '/uploads/';
|
|
||||||
if (!is_dir($targetDir)) {
|
|
||||||
mkdir($targetDir, 0775, true);
|
|
||||||
}
|
|
||||||
$targetFile = $targetDir . 'logo.png';
|
|
||||||
if (move_uploaded_file($_FILES['logo']['tmp_name'], $targetFile)) {
|
|
||||||
$data['logo_path'] = 'uploads/logo.png';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
save_settings($data);
|
|
||||||
$settings = get_settings();
|
|
||||||
$msg = 'Einstellungen gespeichert.';
|
|
||||||
}
|
|
||||||
|
|
||||||
// === JOURNAL TAB ===
|
|
||||||
// Jahr erstellen
|
|
||||||
if ($form === 'year') {
|
|
||||||
$y = (int)($_POST['year'] ?? 0);
|
|
||||||
if ($y >= 2000 && $y <= 2099) {
|
|
||||||
$existing = get_journal_year_by_year($y);
|
|
||||||
if ($existing) {
|
|
||||||
$error = "Jahr $y existiert bereits.";
|
|
||||||
} else {
|
|
||||||
create_journal_year($y, $_POST['notes'] ?? '');
|
|
||||||
$msg = "Jahr $y erstellt.";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$error = 'Ungültiges Jahr.';
|
|
||||||
}
|
|
||||||
$tab = 'journal';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Jahr öffnen/schließen
|
|
||||||
if ($form === 'toggle_year') {
|
|
||||||
$id = (int)($_POST['id'] ?? 0);
|
|
||||||
if ($id) {
|
|
||||||
toggle_journal_year_closed($id);
|
|
||||||
$msg = 'Jahresstatus geändert.';
|
|
||||||
}
|
|
||||||
$tab = 'journal';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lieferant speichern
|
|
||||||
if ($form === 'supplier') {
|
|
||||||
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
|
|
||||||
$data = [
|
|
||||||
'name' => $_POST['name'] ?? '',
|
|
||||||
'sort_order' => $_POST['sort_order'] ?? 0,
|
|
||||||
'is_active' => isset($_POST['is_active']),
|
|
||||||
];
|
|
||||||
if ($data['name']) {
|
|
||||||
save_journal_supplier($id, $data);
|
|
||||||
$msg = 'Lieferant gespeichert.';
|
|
||||||
} else {
|
|
||||||
$error = 'Name ist Pflichtfeld.';
|
|
||||||
}
|
|
||||||
$tab = 'journal';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lieferant löschen
|
|
||||||
if ($form === 'delete_supplier') {
|
|
||||||
delete_journal_supplier((int)$_POST['id']);
|
|
||||||
$msg = 'Lieferant gelöscht.';
|
|
||||||
$tab = 'journal';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wareneingang-Kategorie speichern
|
|
||||||
if ($form === 'rev_cat') {
|
|
||||||
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
|
|
||||||
$data = [
|
|
||||||
'name' => $_POST['name'] ?? '',
|
|
||||||
'category_type' => $_POST['category_type'] ?? 'wareneingang',
|
|
||||||
'vat_rate' => $_POST['vat_rate'] ?? 19,
|
|
||||||
'sort_order' => $_POST['sort_order'] ?? 0,
|
|
||||||
'is_active' => isset($_POST['is_active']),
|
|
||||||
];
|
|
||||||
if ($data['name']) {
|
|
||||||
save_journal_revenue_category($id, $data);
|
|
||||||
$msg = 'Kategorie gespeichert.';
|
|
||||||
} else {
|
|
||||||
$error = 'Name ist Pflichtfeld.';
|
|
||||||
}
|
|
||||||
$tab = 'journal';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wareneingang-/Erlös-Kategorie löschen
|
|
||||||
if ($form === 'delete_rev_cat') {
|
|
||||||
delete_journal_revenue_category((int)$_POST['id']);
|
|
||||||
$msg = 'Kategorie gelöscht.';
|
|
||||||
$tab = 'journal';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aufwandskategorie speichern
|
|
||||||
if ($form === 'exp_cat') {
|
|
||||||
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
|
|
||||||
$data = [
|
|
||||||
'name' => $_POST['name'] ?? '',
|
|
||||||
'side' => $_POST['side'] ?? 'soll',
|
|
||||||
'sort_order' => $_POST['sort_order'] ?? 0,
|
|
||||||
'is_active' => isset($_POST['is_active']),
|
|
||||||
];
|
|
||||||
if ($data['name']) {
|
|
||||||
save_journal_expense_category($id, $data);
|
|
||||||
$msg = 'Aufwandskategorie gespeichert.';
|
|
||||||
} else {
|
|
||||||
$error = 'Name ist Pflichtfeld.';
|
|
||||||
}
|
|
||||||
$tab = 'journal';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aufwandskategorie löschen
|
|
||||||
if ($form === 'delete_exp_cat') {
|
|
||||||
delete_journal_expense_category((int)$_POST['id']);
|
|
||||||
$msg = 'Aufwandskategorie gelöscht.';
|
|
||||||
$tab = 'journal';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Abzug speichern
|
|
||||||
if ($form === 'ded_cat') {
|
|
||||||
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
|
|
||||||
$data = [
|
|
||||||
'name' => $_POST['name'] ?? '',
|
|
||||||
'sort_order' => $_POST['sort_order'] ?? 0,
|
|
||||||
'is_active' => isset($_POST['is_active']),
|
|
||||||
];
|
|
||||||
if ($data['name']) {
|
|
||||||
save_journal_deduction_category($id, $data);
|
|
||||||
$msg = 'Abzug gespeichert.';
|
|
||||||
} else {
|
|
||||||
$error = 'Name ist Pflichtfeld.';
|
|
||||||
}
|
|
||||||
$tab = 'journal';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Abzug löschen
|
|
||||||
if ($form === 'delete_ded_cat') {
|
|
||||||
delete_journal_deduction_category((int)$_POST['id']);
|
|
||||||
$msg = 'Abzug gelöscht.';
|
|
||||||
$tab = 'journal';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zusammenfassungsposten speichern
|
|
||||||
if ($form === 'summary_item') {
|
|
||||||
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
|
|
||||||
$data = [
|
|
||||||
'name' => $_POST['name'] ?? '',
|
|
||||||
'sort_order' => $_POST['sort_order'] ?? 0,
|
|
||||||
'is_active' => isset($_POST['is_active']),
|
|
||||||
];
|
|
||||||
if ($data['name']) {
|
|
||||||
save_journal_summary_item($id, $data);
|
|
||||||
$msg = 'Posten gespeichert.';
|
|
||||||
} else {
|
|
||||||
$error = 'Name ist Pflichtfeld.';
|
|
||||||
}
|
|
||||||
$tab = 'journal';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zusammenfassungsposten löschen
|
|
||||||
if ($form === 'delete_summary_item') {
|
|
||||||
delete_journal_summary_item((int)$_POST['id']);
|
|
||||||
$msg = 'Posten gelöscht.';
|
|
||||||
$tab = 'journal';
|
|
||||||
}
|
|
||||||
|
|
||||||
// === KONTO TAB ===
|
|
||||||
// Benutzername ändern
|
|
||||||
if ($form === 'change_username') {
|
|
||||||
$new_username = trim($_POST['new_username'] ?? '');
|
|
||||||
if (strlen($new_username) < 3) {
|
|
||||||
$error = 'Benutzername muss mindestens 3 Zeichen haben.';
|
|
||||||
} elseif (!update_username($_SESSION['user_id'], $new_username)) {
|
|
||||||
$error = 'Benutzername existiert bereits.';
|
|
||||||
} else {
|
|
||||||
$msg = 'Benutzername geändert.';
|
|
||||||
}
|
|
||||||
$tab = 'konto';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Passwort ändern
|
|
||||||
if ($form === 'change_password') {
|
|
||||||
$current_pw = $_POST['current_password'] ?? '';
|
|
||||||
$new_pw = $_POST['new_password'] ?? '';
|
|
||||||
$confirm_pw = $_POST['confirm_password'] ?? '';
|
|
||||||
if (strlen($new_pw) < 6) {
|
|
||||||
$error = 'Neues Passwort muss mindestens 6 Zeichen haben.';
|
|
||||||
} elseif ($new_pw !== $confirm_pw) {
|
|
||||||
$error = 'Passwörter stimmen nicht überein.';
|
|
||||||
} elseif (!update_password($_SESSION['user_id'], $current_pw, $new_pw)) {
|
|
||||||
$error = 'Aktuelles Passwort ist falsch.';
|
|
||||||
} else {
|
|
||||||
$msg = 'Passwort geändert.';
|
|
||||||
}
|
|
||||||
$tab = 'konto';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Journal-Daten laden
|
|
||||||
$years = get_journal_years();
|
|
||||||
$suppliers = get_journal_suppliers();
|
|
||||||
$we_cats = get_journal_revenue_categories('wareneingang');
|
|
||||||
$er_cats = get_journal_revenue_categories('erloese');
|
|
||||||
$exp_cats = get_journal_expense_categories();
|
|
||||||
$ded_cats = get_journal_deduction_categories();
|
|
||||||
$summary_items = get_journal_summary_items();
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Einstellungen</title>
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>PIRP</h1>
|
|
||||||
<nav>
|
|
||||||
<a href="<?= url_for('index.php') ?>"><?= icon_dashboard() ?>Dashboard</a>
|
|
||||||
<a href="<?= url_for('invoices.php') ?>"><?= icon_invoices() ?>Rechnungen</a>
|
|
||||||
<a href="<?= url_for('customers.php') ?>"><?= icon_customers() ?>Kunden</a>
|
|
||||||
<a href="<?= url_for('expenses.php') ?>"><?= icon_expenses() ?>Ausgaben</a>
|
|
||||||
<a href="<?= url_for('belegarchiv.php') ?>"><?= icon_archive() ?>Belege</a>
|
|
||||||
<a href="<?= url_for('journal.php') ?>"><?= icon_journal() ?>Journal</a>
|
|
||||||
<a href="<?= url_for('euer.php') ?>"><?= icon_euer() ?>EÜR</a>
|
|
||||||
<a href="<?= url_for('settings.php') ?>" class="active"><?= icon_settings() ?>Einstellungen</a>
|
|
||||||
<a href="<?= url_for('logout.php') ?>"><?= icon_logout() ?>Logout (<?= htmlspecialchars($_SESSION['username'] ?? '') ?>)</a>
|
|
||||||
<span class="cmd-k-hint" onclick="document.dispatchEvent(new KeyboardEvent('keydown',{key:'k',ctrlKey:true}))"><kbd>Ctrl+K</kbd></span>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<!-- Tab-Navigation -->
|
|
||||||
<div class="settings-tabs">
|
|
||||||
<a href="<?= url_for('settings.php?tab=allgemein') ?>" class="<?= $tab === 'allgemein' ? 'active' : '' ?>">Allgemein</a>
|
|
||||||
<a href="<?= url_for('settings.php?tab=journal') ?>" class="<?= $tab === 'journal' ? 'active' : '' ?>">Journal</a>
|
|
||||||
<a href="<?= url_for('settings.php?tab=konto') ?>" class="<?= $tab === 'konto' ? 'active' : '' ?>">Konto</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($msg): ?><p class="success"><?= htmlspecialchars($msg) ?></p><?php endif; ?>
|
|
||||||
<?php if ($error): ?><p class="error"><?= htmlspecialchars($error) ?></p><?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($tab === 'allgemein'): ?>
|
|
||||||
<!-- ==================== ALLGEMEIN TAB ==================== -->
|
|
||||||
<section>
|
|
||||||
<h2>Firmeneinstellungen</h2>
|
|
||||||
<form method="post" enctype="multipart/form-data">
|
|
||||||
<input type="hidden" name="form" value="allgemein">
|
|
||||||
<label>Firmenname:
|
|
||||||
<input type="text" name="company_name" value="<?= htmlspecialchars($settings['company_name'] ?? '') ?>">
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>Adresse (mehrzeilig):
|
|
||||||
<textarea name="company_address" rows="3"><?= htmlspecialchars($settings['company_address'] ?? '') ?></textarea>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="flex-row">
|
|
||||||
<label>PLZ:
|
|
||||||
<input type="text" name="company_zip" value="<?= htmlspecialchars($settings['company_zip'] ?? '') ?>">
|
|
||||||
</label>
|
|
||||||
<label>Ort:
|
|
||||||
<input type="text" name="company_city" value="<?= htmlspecialchars($settings['company_city'] ?? '') ?>">
|
|
||||||
</label>
|
|
||||||
<label>Land:
|
|
||||||
<input type="text" name="company_country" value="<?= htmlspecialchars($settings['company_country'] ?? '') ?>">
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label>Steuernummer/USt-IdNr:
|
|
||||||
<input type="text" name="tax_id" value="<?= htmlspecialchars($settings['tax_id'] ?? '') ?>">
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>Umsatzsteuer-Modus:
|
|
||||||
<select name="vat_mode">
|
|
||||||
<option value="klein" <?= ($settings['vat_mode'] ?? '') === 'klein' ? 'selected' : '' ?>>Kleinunternehmer</option>
|
|
||||||
<option value="normal" <?= ($settings['vat_mode'] ?? '') === 'normal' ? 'selected' : '' ?>>Normal</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>Standard-USt-Satz (%):
|
|
||||||
<input type="number" step="0.01" name="default_vat_rate" value="<?= htmlspecialchars($settings['default_vat_rate'] ?? '19.00') ?>">
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>IBAN:
|
|
||||||
<input type="text" name="iban" value="<?= htmlspecialchars($settings['iban'] ?? '') ?>">
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>Telefon:
|
|
||||||
<input type="text" name="phone" value="<?= htmlspecialchars($settings['phone'] ?? '') ?>">
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>E-Mail:
|
|
||||||
<input type="email" name="email" value="<?= htmlspecialchars($settings['email'] ?? '') ?>">
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>Website:
|
|
||||||
<input type="text" name="website" value="<?= htmlspecialchars($settings['website'] ?? '') ?>">
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>Zahlungsbedingungen:
|
|
||||||
<textarea name="payment_terms" rows="2"><?= htmlspecialchars($settings['payment_terms'] ?? '') ?></textarea>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>Fußtext:
|
|
||||||
<textarea name="footer_text" rows="2"><?= htmlspecialchars($settings['footer_text'] ?? '') ?></textarea>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>Logo (PNG):
|
|
||||||
<input type="file" name="logo" accept="image/png">
|
|
||||||
</label>
|
|
||||||
<?php if (!empty($settings['logo_path'])): ?>
|
|
||||||
<p>Aktuelles Logo:<br>
|
|
||||||
<img src="<?= htmlspecialchars($settings['logo_path']) ?>" style="max-height:60px;"></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<button type="submit">Speichern</button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php elseif ($tab === 'journal'): ?>
|
|
||||||
<!-- ==================== JOURNAL TAB ==================== -->
|
|
||||||
|
|
||||||
<!-- Journal Sub-Tabs -->
|
|
||||||
<div class="journal-settings-subtabs">
|
|
||||||
<a href="<?= url_for('settings.php?tab=journal&jsub=jahre') ?>" class="<?= $journal_sub === 'jahre' ? 'active' : '' ?>">Jahre</a>
|
|
||||||
<a href="<?= url_for('settings.php?tab=journal&jsub=einnahmen') ?>" class="<?= $journal_sub === 'einnahmen' ? 'active' : '' ?>">Einnahmen</a>
|
|
||||||
<a href="<?= url_for('settings.php?tab=journal&jsub=ausgaben') ?>" class="<?= $journal_sub === 'ausgaben' ? 'active' : '' ?>">Ausgaben</a>
|
|
||||||
<a href="<?= url_for('settings.php?tab=journal&jsub=stammdaten') ?>" class="<?= $journal_sub === 'stammdaten' ? 'active' : '' ?>">Sonstiges</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($journal_sub === 'jahre'): ?>
|
|
||||||
<!-- ========== JAHRE ========== -->
|
|
||||||
<section>
|
|
||||||
<h2>Journal-Jahre</h2>
|
|
||||||
<p class="settings-help">Hier verwalten Sie die Buchungsjahre. Ein geschlossenes Jahr kann nicht mehr bearbeitet werden.</p>
|
|
||||||
<div>
|
|
||||||
<form method="post" class="flex-row" style="margin-bottom:12px;">
|
|
||||||
<input type="hidden" name="form" value="year">
|
|
||||||
<label>Jahr:
|
|
||||||
<input type="number" name="year" value="<?= date('Y') ?>" min="2000" max="2099" required style="max-width:100px;">
|
|
||||||
</label>
|
|
||||||
<label>Notizen:
|
|
||||||
<input type="text" name="notes" value="">
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<button type="submit">Jahr erstellen</button>
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
<?php if ($years): ?>
|
|
||||||
<table class="list">
|
|
||||||
<thead><tr><th>Jahr</th><th>Status</th><th>Notizen</th><th>Aktion</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($years as $y): ?>
|
|
||||||
<tr>
|
|
||||||
<td><strong><?= (int)$y['year'] ?></strong></td>
|
|
||||||
<td><?= $y['is_closed'] ? '<span class="badge badge-danger">Geschlossen</span>' : '<span class="badge badge-success">Offen</span>' ?></td>
|
|
||||||
<td><?= htmlspecialchars($y['notes'] ?? '') ?></td>
|
|
||||||
<td>
|
|
||||||
<form method="post" style="display:inline;">
|
|
||||||
<input type="hidden" name="form" value="toggle_year">
|
|
||||||
<input type="hidden" name="id" value="<?= $y['id'] ?>">
|
|
||||||
<button type="submit" class="secondary" style="padding:3px 8px;font-size:10px;">
|
|
||||||
<?= $y['is_closed'] ? 'Öffnen' : 'Schließen' ?>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<a href="<?= url_for('journal.php?year_id=' . $y['id']) ?>" style="margin-left:6px;font-size:10px;">Zum Journal</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<?php else: ?>
|
|
||||||
<p class="info">Noch keine Jahre angelegt. Erstellen Sie ein Jahr, um mit der Buchführung zu beginnen.</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php elseif ($journal_sub === 'einnahmen'): ?>
|
|
||||||
<!-- ========== EINNAHMEN ========== -->
|
|
||||||
|
|
||||||
<!-- Erlös-Kategorien -->
|
|
||||||
<section>
|
|
||||||
<h2>Erlös-Kategorien</h2>
|
|
||||||
<p class="settings-help">Kategorien für Einnahmen/Erlöse (z.B. "Umsatz 7%", "Umsatz 19%"). Diese erscheinen als Spalten im Journal.</p>
|
|
||||||
<div>
|
|
||||||
<form method="post" class="flex-row" style="margin-bottom:12px;">
|
|
||||||
<input type="hidden" name="form" value="rev_cat">
|
|
||||||
<input type="hidden" name="category_type" value="erloese">
|
|
||||||
<label>Name:
|
|
||||||
<input type="text" name="name" required placeholder="z.B. Umsatz 19%">
|
|
||||||
</label>
|
|
||||||
<label>MwSt %:
|
|
||||||
<input type="number" step="0.01" name="vat_rate" value="19" style="max-width:80px;">
|
|
||||||
</label>
|
|
||||||
<label>Sort.:
|
|
||||||
<input type="number" name="sort_order" value="0" style="max-width:60px;">
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" name="is_active" checked> Aktiv
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<button type="submit">Hinzufügen</button>
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
<?php if ($er_cats): ?>
|
|
||||||
<table class="list">
|
|
||||||
<thead><tr><th>Name</th><th>MwSt</th><th>Sort.</th><th>Aktiv</th><th>Aktion</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($er_cats as $cat): ?>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<form method="post" style="display:inline;" class="flex-row">
|
|
||||||
<input type="hidden" name="form" value="rev_cat">
|
|
||||||
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
|
|
||||||
<input type="hidden" name="category_type" value="erloese">
|
|
||||||
<input type="text" name="name" value="<?= htmlspecialchars($cat['name']) ?>" style="max-width:150px;">
|
|
||||||
</td>
|
|
||||||
<td><input type="number" step="0.01" name="vat_rate" value="<?= htmlspecialchars($cat['vat_rate']) ?>" style="max-width:70px;"></td>
|
|
||||||
<td><input type="number" name="sort_order" value="<?= (int)$cat['sort_order'] ?>" style="max-width:50px;"></td>
|
|
||||||
<td><input type="checkbox" name="is_active" <?= $cat['is_active'] ? 'checked' : '' ?>></td>
|
|
||||||
<td>
|
|
||||||
<button type="submit" class="secondary" style="padding:3px 8px;font-size:10px;">Speichern</button>
|
|
||||||
</form>
|
|
||||||
<form method="post" style="display:inline;">
|
|
||||||
<input type="hidden" name="form" value="delete_rev_cat">
|
|
||||||
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
|
|
||||||
<button type="submit" class="danger" style="padding:3px 8px;font-size:10px;" onclick="return confirm('Wirklich löschen?');">X</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Wareneingang-Kategorien -->
|
|
||||||
<section>
|
|
||||||
<h2>Wareneingang-Kategorien</h2>
|
|
||||||
<p class="settings-help">Kategorien für Wareneinkauf (z.B. "WE 7%", "WE 19%"). Diese erscheinen als Spalten im Journal.</p>
|
|
||||||
<div>
|
|
||||||
<form method="post" class="flex-row" style="margin-bottom:12px;">
|
|
||||||
<input type="hidden" name="form" value="rev_cat">
|
|
||||||
<input type="hidden" name="category_type" value="wareneingang">
|
|
||||||
<label>Name:
|
|
||||||
<input type="text" name="name" required placeholder="z.B. WE 19%">
|
|
||||||
</label>
|
|
||||||
<label>MwSt %:
|
|
||||||
<input type="number" step="0.01" name="vat_rate" value="19" style="max-width:80px;">
|
|
||||||
</label>
|
|
||||||
<label>Sort.:
|
|
||||||
<input type="number" name="sort_order" value="0" style="max-width:60px;">
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" name="is_active" checked> Aktiv
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<button type="submit">Hinzufügen</button>
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
<?php if ($we_cats): ?>
|
|
||||||
<table class="list">
|
|
||||||
<thead><tr><th>Name</th><th>MwSt</th><th>Sort.</th><th>Aktiv</th><th>Aktion</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($we_cats as $cat): ?>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<form method="post" style="display:inline;" class="flex-row">
|
|
||||||
<input type="hidden" name="form" value="rev_cat">
|
|
||||||
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
|
|
||||||
<input type="hidden" name="category_type" value="wareneingang">
|
|
||||||
<input type="text" name="name" value="<?= htmlspecialchars($cat['name']) ?>" style="max-width:150px;">
|
|
||||||
</td>
|
|
||||||
<td><input type="number" step="0.01" name="vat_rate" value="<?= htmlspecialchars($cat['vat_rate']) ?>" style="max-width:70px;"></td>
|
|
||||||
<td><input type="number" name="sort_order" value="<?= (int)$cat['sort_order'] ?>" style="max-width:50px;"></td>
|
|
||||||
<td><input type="checkbox" name="is_active" <?= $cat['is_active'] ? 'checked' : '' ?>></td>
|
|
||||||
<td>
|
|
||||||
<button type="submit" class="secondary" style="padding:3px 8px;font-size:10px;">Speichern</button>
|
|
||||||
</form>
|
|
||||||
<form method="post" style="display:inline;">
|
|
||||||
<input type="hidden" name="form" value="delete_rev_cat">
|
|
||||||
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
|
|
||||||
<button type="submit" class="danger" style="padding:3px 8px;font-size:10px;" onclick="return confirm('Wirklich löschen?');">X</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php elseif ($journal_sub === 'ausgaben'): ?>
|
|
||||||
<!-- ========== AUSGABEN / ABZÜGE ========== -->
|
|
||||||
|
|
||||||
<!-- Aufwandskategorien -->
|
|
||||||
<section>
|
|
||||||
<h2>Aufwandskategorien</h2>
|
|
||||||
<p class="settings-help">Kategorien für Betriebsausgaben (z.B. "Miete", "Versicherung", "Telefon"). Diese erscheinen als Spalten im Journal und werden in der EÜR berücksichtigt.</p>
|
|
||||||
<div>
|
|
||||||
<form method="post" class="flex-row" style="margin-bottom:12px;">
|
|
||||||
<input type="hidden" name="form" value="exp_cat">
|
|
||||||
<label>Name:
|
|
||||||
<input type="text" name="name" required placeholder="z.B. Miete">
|
|
||||||
</label>
|
|
||||||
<label>Typ:
|
|
||||||
<select name="side" style="max-width:120px;">
|
|
||||||
<option value="soll">Soll</option>
|
|
||||||
<option value="soll_haben">Soll+Haben</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>Sort.:
|
|
||||||
<input type="number" name="sort_order" value="0" style="max-width:60px;">
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" name="is_active" checked> Aktiv
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<button type="submit">Hinzufügen</button>
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
<?php if ($exp_cats): ?>
|
|
||||||
<table class="list">
|
|
||||||
<thead><tr><th>Name</th><th>Typ</th><th>Sort.</th><th>Aktiv</th><th>Aktion</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($exp_cats as $cat): ?>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<form method="post" style="display:inline;" class="flex-row">
|
|
||||||
<input type="hidden" name="form" value="exp_cat">
|
|
||||||
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
|
|
||||||
<input type="text" name="name" value="<?= htmlspecialchars($cat['name']) ?>" style="max-width:150px;">
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<select name="side" style="max-width:100px;">
|
|
||||||
<option value="soll" <?= $cat['side'] === 'soll' ? 'selected' : '' ?>>Soll</option>
|
|
||||||
<option value="soll_haben" <?= $cat['side'] === 'soll_haben' ? 'selected' : '' ?>>Soll+Haben</option>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td><input type="number" name="sort_order" value="<?= (int)$cat['sort_order'] ?>" style="max-width:50px;"></td>
|
|
||||||
<td><input type="checkbox" name="is_active" <?= $cat['is_active'] ? 'checked' : '' ?>></td>
|
|
||||||
<td>
|
|
||||||
<button type="submit" class="secondary" style="padding:3px 8px;font-size:10px;">Speichern</button>
|
|
||||||
</form>
|
|
||||||
<form method="post" style="display:inline;">
|
|
||||||
<input type="hidden" name="form" value="delete_exp_cat">
|
|
||||||
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
|
|
||||||
<button type="submit" class="danger" style="padding:3px 8px;font-size:10px;" onclick="return confirm('Wirklich löschen?');">X</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Abzüge -->
|
|
||||||
<section>
|
|
||||||
<h2>Abzüge</h2>
|
|
||||||
<p class="settings-help">Abzugskategorien (z.B. "Skonto", "Lotto"). Diese erscheinen als Haben-Spalten im Journal und werden in der EÜR berücksichtigt.</p>
|
|
||||||
<div>
|
|
||||||
<form method="post" class="flex-row" style="margin-bottom:12px;">
|
|
||||||
<input type="hidden" name="form" value="ded_cat">
|
|
||||||
<label>Name:
|
|
||||||
<input type="text" name="name" required placeholder="z.B. Skonto">
|
|
||||||
</label>
|
|
||||||
<label>Sort.:
|
|
||||||
<input type="number" name="sort_order" value="0" style="max-width:60px;">
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" name="is_active" checked> Aktiv
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<button type="submit">Hinzufügen</button>
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
<?php if ($ded_cats): ?>
|
|
||||||
<table class="list">
|
|
||||||
<thead><tr><th>Name</th><th>Sort.</th><th>Aktiv</th><th>Aktion</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($ded_cats as $cat): ?>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<form method="post" style="display:inline;" class="flex-row">
|
|
||||||
<input type="hidden" name="form" value="ded_cat">
|
|
||||||
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
|
|
||||||
<input type="text" name="name" value="<?= htmlspecialchars($cat['name']) ?>" style="max-width:200px;">
|
|
||||||
</td>
|
|
||||||
<td><input type="number" name="sort_order" value="<?= (int)$cat['sort_order'] ?>" style="max-width:50px;"></td>
|
|
||||||
<td><input type="checkbox" name="is_active" <?= $cat['is_active'] ? 'checked' : '' ?>></td>
|
|
||||||
<td>
|
|
||||||
<button type="submit" class="secondary" style="padding:3px 8px;font-size:10px;">Speichern</button>
|
|
||||||
</form>
|
|
||||||
<form method="post" style="display:inline;">
|
|
||||||
<input type="hidden" name="form" value="delete_ded_cat">
|
|
||||||
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
|
|
||||||
<button type="submit" class="danger" style="padding:3px 8px;font-size:10px;" onclick="return confirm('Wirklich löschen?');">X</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php elseif ($journal_sub === 'stammdaten'): ?>
|
|
||||||
<!-- ========== STAMMDATEN ========== -->
|
|
||||||
|
|
||||||
<!-- Lieferanten -->
|
|
||||||
<section>
|
|
||||||
<h2>Lieferanten</h2>
|
|
||||||
<p class="settings-help">Lieferanten können bei Buchungen ausgewählt werden, um die Zuordnung zu erleichtern.</p>
|
|
||||||
<div>
|
|
||||||
<form method="post" class="flex-row" style="margin-bottom:12px;">
|
|
||||||
<input type="hidden" name="form" value="supplier">
|
|
||||||
<label>Name:
|
|
||||||
<input type="text" name="name" required placeholder="z.B. Metro">
|
|
||||||
</label>
|
|
||||||
<label>Sort.:
|
|
||||||
<input type="number" name="sort_order" value="0" style="max-width:60px;">
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" name="is_active" checked> Aktiv
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<button type="submit">Hinzufügen</button>
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
<?php if ($suppliers): ?>
|
|
||||||
<table class="list">
|
|
||||||
<thead><tr><th>Name</th><th>Sort.</th><th>Aktiv</th><th>Aktion</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($suppliers as $sup): ?>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<form method="post" style="display:inline;" class="flex-row">
|
|
||||||
<input type="hidden" name="form" value="supplier">
|
|
||||||
<input type="hidden" name="id" value="<?= $sup['id'] ?>">
|
|
||||||
<input type="text" name="name" value="<?= htmlspecialchars($sup['name']) ?>" style="max-width:200px;">
|
|
||||||
</td>
|
|
||||||
<td><input type="number" name="sort_order" value="<?= (int)$sup['sort_order'] ?>" style="max-width:50px;"></td>
|
|
||||||
<td><input type="checkbox" name="is_active" <?= $sup['is_active'] ? 'checked' : '' ?>></td>
|
|
||||||
<td>
|
|
||||||
<button type="submit" class="secondary" style="padding:3px 8px;font-size:10px;">Speichern</button>
|
|
||||||
</form>
|
|
||||||
<form method="post" style="display:inline;">
|
|
||||||
<input type="hidden" name="form" value="delete_supplier">
|
|
||||||
<input type="hidden" name="id" value="<?= $sup['id'] ?>">
|
|
||||||
<button type="submit" class="danger" style="padding:3px 8px;font-size:10px;" onclick="return confirm('Wirklich löschen?');">X</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Umsatz-Zusammenfassungsposten -->
|
|
||||||
<section>
|
|
||||||
<h2>Umsatz-Zusammenfassungsposten</h2>
|
|
||||||
<p class="settings-help">Zusätzliche Posten für die monatliche Umsatzübersicht im Journal (z.B. "Reinigung", "RMV").</p>
|
|
||||||
<div>
|
|
||||||
<form method="post" class="flex-row" style="margin-bottom:12px;">
|
|
||||||
<input type="hidden" name="form" value="summary_item">
|
|
||||||
<label>Name:
|
|
||||||
<input type="text" name="name" required placeholder="z.B. Reinigung">
|
|
||||||
</label>
|
|
||||||
<label>Sort.:
|
|
||||||
<input type="number" name="sort_order" value="0" style="max-width:60px;">
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" name="is_active" checked> Aktiv
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<button type="submit">Hinzufügen</button>
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
<?php if ($summary_items): ?>
|
|
||||||
<table class="list">
|
|
||||||
<thead><tr><th>Name</th><th>Sort.</th><th>Aktiv</th><th>Aktion</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($summary_items as $item): ?>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<form method="post" style="display:inline;" class="flex-row">
|
|
||||||
<input type="hidden" name="form" value="summary_item">
|
|
||||||
<input type="hidden" name="id" value="<?= $item['id'] ?>">
|
|
||||||
<input type="text" name="name" value="<?= htmlspecialchars($item['name']) ?>" style="max-width:200px;">
|
|
||||||
</td>
|
|
||||||
<td><input type="number" name="sort_order" value="<?= (int)$item['sort_order'] ?>" style="max-width:50px;"></td>
|
|
||||||
<td><input type="checkbox" name="is_active" <?= $item['is_active'] ? 'checked' : '' ?>></td>
|
|
||||||
<td>
|
|
||||||
<button type="submit" class="secondary" style="padding:3px 8px;font-size:10px;">Speichern</button>
|
|
||||||
</form>
|
|
||||||
<form method="post" style="display:inline;">
|
|
||||||
<input type="hidden" name="form" value="delete_summary_item">
|
|
||||||
<input type="hidden" name="id" value="<?= $item['id'] ?>">
|
|
||||||
<button type="submit" class="danger" style="padding:3px 8px;font-size:10px;" onclick="return confirm('Wirklich löschen?');">X</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php elseif ($tab === 'konto'): ?>
|
|
||||||
<!-- ==================== KONTO TAB ==================== -->
|
|
||||||
<?php $current_user = get_logged_in_user(); ?>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>Benutzername ändern</h2>
|
|
||||||
<div>
|
|
||||||
<form method="post">
|
|
||||||
<input type="hidden" name="form" value="change_username">
|
|
||||||
<label>Aktueller Benutzername:
|
|
||||||
<input type="text" value="<?= htmlspecialchars($current_user['username'] ?? '') ?>" disabled>
|
|
||||||
</label>
|
|
||||||
<label>Neuer Benutzername:
|
|
||||||
<input type="text" name="new_username" required minlength="3" style="max-width:300px;">
|
|
||||||
</label>
|
|
||||||
<button type="submit">Benutzername ändern</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>Passwort ändern</h2>
|
|
||||||
<div>
|
|
||||||
<form method="post">
|
|
||||||
<input type="hidden" name="form" value="change_password">
|
|
||||||
<label>Aktuelles Passwort:
|
|
||||||
<input type="password" name="current_password" required style="max-width:300px;">
|
|
||||||
</label>
|
|
||||||
<label>Neues Passwort:
|
|
||||||
<input type="password" name="new_password" required minlength="6" style="max-width:300px;">
|
|
||||||
</label>
|
|
||||||
<label>Neues Passwort bestätigen:
|
|
||||||
<input type="password" name="confirm_password" required minlength="6" style="max-width:300px;">
|
|
||||||
</label>
|
|
||||||
<button type="submit">Passwort ändern</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php endif; ?>
|
|
||||||
</main>
|
|
||||||
<script src="assets/command-palette.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 469 KiB |
113
pirp/reset.sh
113
pirp/reset.sh
@@ -1,113 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# PIRP Reset Script - Setzt DB und Uploads komplett zurück
|
|
||||||
# Default Login nach Reset: admin:admin
|
|
||||||
#
|
|
||||||
# Verwendung:
|
|
||||||
# ./reset.sh - Interaktiv (fragt nach Docker/Lokal)
|
|
||||||
# ./reset.sh docker - Nur Docker
|
|
||||||
# ./reset.sh local - Nur Lokal
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
|
|
||||||
# DB-Verbindungsdaten für lokalen Betrieb (aus config.php oder Env)
|
|
||||||
DB_HOST="${DB_HOST:-127.0.0.1}"
|
|
||||||
DB_PORT="${DB_PORT:-5432}"
|
|
||||||
DB_NAME="${DB_NAME:-pirp}"
|
|
||||||
DB_USER="${DB_USER:-pirp_user}"
|
|
||||||
DB_PASS="${DB_PASS:-PIRPdb2025!}"
|
|
||||||
|
|
||||||
# admin:admin Password Hash
|
|
||||||
ADMIN_HASH='$2y$10$YourHashHere'
|
|
||||||
|
|
||||||
echo "=== PIRP Reset ==="
|
|
||||||
echo "WARNUNG: Dies löscht ALLE Daten (Datenbank + Uploads)!"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Modus bestimmen
|
|
||||||
MODE="$1"
|
|
||||||
if [ -z "$MODE" ]; then
|
|
||||||
echo "Welchen Modus verwenden?"
|
|
||||||
echo " 1) Docker (docker-compose)"
|
|
||||||
echo " 2) Lokal (PostgreSQL direkt)"
|
|
||||||
echo ""
|
|
||||||
read -p "Auswahl (1/2): " choice
|
|
||||||
case "$choice" in
|
|
||||||
1) MODE="docker" ;;
|
|
||||||
2) MODE="local" ;;
|
|
||||||
*) echo "Ungültige Auswahl."; exit 1 ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
read -p "Wirklich ALLE Daten löschen? (ja/nein): " confirm
|
|
||||||
if [ "$confirm" != "ja" ]; then
|
|
||||||
echo "Abgebrochen."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Uploads löschen
|
|
||||||
echo ""
|
|
||||||
echo "=> Uploads löschen..."
|
|
||||||
rm -rf public/uploads/logos/* 2>/dev/null || true
|
|
||||||
rm -rf public/uploads/expenses/* 2>/dev/null || true
|
|
||||||
rm -rf public/uploads/invoices/* 2>/dev/null || true
|
|
||||||
mkdir -p public/uploads/logos public/uploads/expenses public/uploads/invoices
|
|
||||||
echo " Uploads gelöscht."
|
|
||||||
|
|
||||||
if [ "$MODE" = "docker" ]; then
|
|
||||||
# === DOCKER MODUS ===
|
|
||||||
echo ""
|
|
||||||
echo "=> Docker Container stoppen und Volume löschen..."
|
|
||||||
docker compose down -v 2>/dev/null || docker-compose down -v 2>/dev/null || true
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=> Docker Container neu starten..."
|
|
||||||
docker compose up -d 2>/dev/null || docker-compose up -d
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=> Warte auf Datenbank..."
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=> Migrationen ausführen..."
|
|
||||||
docker compose exec -T db psql -U "$DB_USER" -d "$DB_NAME" -f /docker-entrypoint-initdb.d/02-journal.sql 2>/dev/null || true
|
|
||||||
|
|
||||||
else
|
|
||||||
# === LOKAL MODUS ===
|
|
||||||
export PGPASSWORD="$DB_PASS"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=> Datenbank zurücksetzen..."
|
|
||||||
|
|
||||||
# Alle Tabellen droppen und Schema neu erstellen
|
|
||||||
echo " Schema anwenden..."
|
|
||||||
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f schema.sql
|
|
||||||
|
|
||||||
echo " Journal-Migration..."
|
|
||||||
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f tools/migrate_journal.sql
|
|
||||||
|
|
||||||
# Admin-User erstellen (admin:admin)
|
|
||||||
echo " Admin-User erstellen..."
|
|
||||||
ADMIN_HASH=$(php -r "echo password_hash('admin', PASSWORD_DEFAULT);")
|
|
||||||
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" <<EOF
|
|
||||||
INSERT INTO users (username, password_hash)
|
|
||||||
VALUES ('admin', '$ADMIN_HASH')
|
|
||||||
ON CONFLICT (username) DO UPDATE SET password_hash = EXCLUDED.password_hash;
|
|
||||||
EOF
|
|
||||||
|
|
||||||
unset PGPASSWORD
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Reset abgeschlossen ==="
|
|
||||||
echo ""
|
|
||||||
echo "Login: admin / admin"
|
|
||||||
if [ "$MODE" = "docker" ]; then
|
|
||||||
echo "URL: http://localhost:8080"
|
|
||||||
else
|
|
||||||
echo "Starte Server mit: php -S localhost:8080 -t public"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
270
pirp/schema.sql
270
pirp/schema.sql
@@ -1,270 +0,0 @@
|
|||||||
-- PIRP vollständiges Schema (inkl. Journal-Modul)
|
|
||||||
|
|
||||||
-- Reihenfolge beachten wegen Foreign Keys
|
|
||||||
DROP TABLE IF EXISTS journal_monthly_summary_values CASCADE;
|
|
||||||
DROP TABLE IF EXISTS journal_monthly_summary CASCADE;
|
|
||||||
DROP TABLE IF EXISTS journal_entry_accounts CASCADE;
|
|
||||||
DROP TABLE IF EXISTS journal_entries CASCADE;
|
|
||||||
DROP TABLE IF EXISTS journal_summary_items CASCADE;
|
|
||||||
DROP TABLE IF EXISTS journal_deduction_categories CASCADE;
|
|
||||||
DROP TABLE IF EXISTS journal_expense_categories CASCADE;
|
|
||||||
DROP TABLE IF EXISTS journal_revenue_categories CASCADE;
|
|
||||||
DROP TABLE IF EXISTS journal_suppliers CASCADE;
|
|
||||||
DROP TABLE IF EXISTS journal_years CASCADE;
|
|
||||||
DROP TABLE IF EXISTS recurring_log CASCADE;
|
|
||||||
DROP TABLE IF EXISTS recurring_template_items CASCADE;
|
|
||||||
DROP TABLE IF EXISTS recurring_templates CASCADE;
|
|
||||||
DROP TABLE IF EXISTS invoice_items CASCADE;
|
|
||||||
DROP TABLE IF EXISTS invoices CASCADE;
|
|
||||||
DROP TABLE IF EXISTS expenses CASCADE;
|
|
||||||
DROP TABLE IF EXISTS customers CASCADE;
|
|
||||||
DROP TABLE IF EXISTS settings CASCADE;
|
|
||||||
DROP TABLE IF EXISTS users CASCADE;
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Basis-Tabellen
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
CREATE TABLE users (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
username VARCHAR(50) UNIQUE NOT NULL,
|
|
||||||
password_hash TEXT NOT NULL,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE settings (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
company_name TEXT,
|
|
||||||
company_address TEXT,
|
|
||||||
company_zip TEXT,
|
|
||||||
company_city TEXT,
|
|
||||||
company_country TEXT,
|
|
||||||
tax_id TEXT,
|
|
||||||
vat_mode VARCHAR(10) DEFAULT 'klein',
|
|
||||||
default_vat_rate NUMERIC(5,2) DEFAULT 19.00,
|
|
||||||
payment_terms TEXT,
|
|
||||||
footer_text TEXT,
|
|
||||||
logo_path TEXT,
|
|
||||||
iban TEXT,
|
|
||||||
phone TEXT,
|
|
||||||
email TEXT,
|
|
||||||
website TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE customers (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
customer_number VARCHAR(20) UNIQUE,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
address TEXT,
|
|
||||||
zip TEXT,
|
|
||||||
city TEXT,
|
|
||||||
country TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Rechnungen
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
CREATE TABLE invoices (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
invoice_number VARCHAR(50) UNIQUE NOT NULL,
|
|
||||||
customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE RESTRICT,
|
|
||||||
invoice_date DATE NOT NULL,
|
|
||||||
service_date DATE,
|
|
||||||
vat_mode VARCHAR(10) NOT NULL DEFAULT 'klein',
|
|
||||||
vat_rate NUMERIC(5,2) NOT NULL DEFAULT 19.00,
|
|
||||||
payment_terms TEXT,
|
|
||||||
notes_internal TEXT,
|
|
||||||
total_net NUMERIC(12,2) NOT NULL DEFAULT 0,
|
|
||||||
total_vat NUMERIC(12,2) NOT NULL DEFAULT 0,
|
|
||||||
total_gross NUMERIC(12,2) NOT NULL DEFAULT 0,
|
|
||||||
paid BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
payment_date DATE,
|
|
||||||
pdf_path TEXT,
|
|
||||||
pdf_hash VARCHAR(64),
|
|
||||||
pdf_generated_at TIMESTAMPTZ,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE invoice_items (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
invoice_id INTEGER NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
|
|
||||||
position_no INTEGER NOT NULL,
|
|
||||||
description TEXT NOT NULL,
|
|
||||||
quantity NUMERIC(12,2) NOT NULL DEFAULT 1,
|
|
||||||
unit_price NUMERIC(12,2) NOT NULL DEFAULT 0,
|
|
||||||
vat_rate NUMERIC(5,2) NOT NULL DEFAULT 19.00
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Ausgaben
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
CREATE TABLE expenses (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
expense_date DATE NOT NULL,
|
|
||||||
description TEXT NOT NULL,
|
|
||||||
category TEXT,
|
|
||||||
amount NUMERIC(12,2) NOT NULL,
|
|
||||||
vat_rate NUMERIC(5,2) DEFAULT 0,
|
|
||||||
total_net NUMERIC(12,2),
|
|
||||||
total_vat NUMERIC(12,2) DEFAULT 0,
|
|
||||||
expense_category_id INTEGER,
|
|
||||||
paid BOOLEAN NOT NULL DEFAULT TRUE,
|
|
||||||
payment_date DATE,
|
|
||||||
attachment_path TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Wiederkehrende Rechnungen (Abo-Rechnungen)
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
CREATE TABLE recurring_templates (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
template_name VARCHAR(100) NOT NULL,
|
|
||||||
customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE RESTRICT,
|
|
||||||
interval_type VARCHAR(20) NOT NULL CHECK (interval_type IN ('monthly', 'quarterly', 'yearly')),
|
|
||||||
start_date DATE NOT NULL,
|
|
||||||
end_date DATE,
|
|
||||||
next_due_date DATE NOT NULL,
|
|
||||||
vat_mode VARCHAR(10) NOT NULL DEFAULT 'klein',
|
|
||||||
vat_rate NUMERIC(5,2) NOT NULL DEFAULT 19.00,
|
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
||||||
notes_internal TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE recurring_template_items (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
template_id INTEGER NOT NULL REFERENCES recurring_templates(id) ON DELETE CASCADE,
|
|
||||||
position_no INTEGER NOT NULL,
|
|
||||||
description TEXT NOT NULL,
|
|
||||||
quantity NUMERIC(12,2) NOT NULL DEFAULT 1,
|
|
||||||
unit_price NUMERIC(12,2) NOT NULL DEFAULT 0
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE recurring_log (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
template_id INTEGER NOT NULL REFERENCES recurring_templates(id) ON DELETE CASCADE,
|
|
||||||
invoice_id INTEGER REFERENCES invoices(id) ON DELETE SET NULL,
|
|
||||||
generated_at TIMESTAMPTZ DEFAULT now(),
|
|
||||||
due_date DATE NOT NULL,
|
|
||||||
status VARCHAR(20) NOT NULL DEFAULT 'generated'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Journal-Modul: Doppelte Buchführung
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
CREATE TABLE journal_years (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
year INTEGER NOT NULL UNIQUE,
|
|
||||||
is_closed BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
notes TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE journal_suppliers (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE journal_revenue_categories (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
category_type VARCHAR(20) NOT NULL CHECK (category_type IN ('wareneingang', 'erloese')),
|
|
||||||
vat_rate NUMERIC(5,2) NOT NULL DEFAULT 19.00,
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE journal_expense_categories (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
side VARCHAR(10) NOT NULL DEFAULT 'soll' CHECK (side IN ('soll', 'soll_haben')),
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE journal_deduction_categories (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE journal_entries (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
year_id INTEGER NOT NULL REFERENCES journal_years(id) ON DELETE RESTRICT,
|
|
||||||
entry_date DATE NOT NULL,
|
|
||||||
month INTEGER NOT NULL CHECK (month BETWEEN 1 AND 12),
|
|
||||||
description TEXT NOT NULL,
|
|
||||||
attachment_note TEXT,
|
|
||||||
amount NUMERIC(12,2) NOT NULL DEFAULT 0,
|
|
||||||
supplier_id INTEGER REFERENCES journal_suppliers(id) ON DELETE SET NULL,
|
|
||||||
invoice_id INTEGER REFERENCES invoices(id) ON DELETE SET NULL,
|
|
||||||
expense_id INTEGER REFERENCES expenses(id) ON DELETE SET NULL,
|
|
||||||
source_type VARCHAR(20) DEFAULT 'manual',
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE journal_entry_accounts (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
entry_id INTEGER NOT NULL REFERENCES journal_entries(id) ON DELETE CASCADE,
|
|
||||||
account_type VARCHAR(20) NOT NULL,
|
|
||||||
side VARCHAR(5) NOT NULL CHECK (side IN ('soll', 'haben')),
|
|
||||||
amount NUMERIC(12,2) NOT NULL DEFAULT 0,
|
|
||||||
revenue_category_id INTEGER REFERENCES journal_revenue_categories(id) ON DELETE SET NULL,
|
|
||||||
expense_category_id INTEGER REFERENCES journal_expense_categories(id) ON DELETE SET NULL,
|
|
||||||
note TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE journal_monthly_summary (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
year_id INTEGER NOT NULL REFERENCES journal_years(id) ON DELETE CASCADE,
|
|
||||||
month INTEGER NOT NULL CHECK (month BETWEEN 1 AND 12),
|
|
||||||
manual_corrections JSONB DEFAULT '{}',
|
|
||||||
notes TEXT,
|
|
||||||
UNIQUE(year_id, month)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE journal_summary_items (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE journal_monthly_summary_values (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
year_id INTEGER NOT NULL REFERENCES journal_years(id) ON DELETE CASCADE,
|
|
||||||
month INTEGER NOT NULL CHECK (month BETWEEN 1 AND 12),
|
|
||||||
summary_item_id INTEGER NOT NULL REFERENCES journal_summary_items(id) ON DELETE CASCADE,
|
|
||||||
amount NUMERIC(12,2) NOT NULL DEFAULT 0,
|
|
||||||
UNIQUE(year_id, month, summary_item_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Foreign Key für expenses -> journal_expense_categories (nachträglich, da Tabelle erst jetzt existiert)
|
|
||||||
ALTER TABLE expenses ADD CONSTRAINT fk_expenses_category
|
|
||||||
FOREIGN KEY (expense_category_id) REFERENCES journal_expense_categories(id) ON DELETE SET NULL;
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Indizes
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
CREATE INDEX idx_journal_entries_year_month ON journal_entries(year_id, month);
|
|
||||||
CREATE INDEX idx_journal_entries_date ON journal_entries(entry_date);
|
|
||||||
CREATE INDEX idx_journal_entries_invoice ON journal_entries(invoice_id);
|
|
||||||
CREATE INDEX idx_journal_entries_expense ON journal_entries(expense_id);
|
|
||||||
CREATE INDEX idx_journal_entries_source ON journal_entries(source_type);
|
|
||||||
|
|
||||||
-- Unique-Constraints: max. 1 Journal-Eintrag pro Rechnung/Ausgabe (verhindert Doppelbuchungen)
|
|
||||||
CREATE UNIQUE INDEX idx_unique_journal_invoice ON journal_entries(invoice_id) WHERE invoice_id IS NOT NULL;
|
|
||||||
CREATE UNIQUE INDEX idx_unique_journal_expense ON journal_entries(expense_id) WHERE expense_id IS NOT NULL;
|
|
||||||
CREATE INDEX idx_journal_entry_accounts_entry ON journal_entry_accounts(entry_id);
|
|
||||||
CREATE INDEX idx_journal_entry_accounts_type ON journal_entry_accounts(account_type);
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/db.php';
|
|
||||||
|
|
||||||
function login(string $username, string $password): bool {
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->prepare('SELECT id, username, password_hash FROM users WHERE username = :u');
|
|
||||||
$stmt->execute([':u' => $username]);
|
|
||||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
if ($user && password_verify($password, $user['password_hash'])) {
|
|
||||||
$_SESSION['user_id'] = $user['id'];
|
|
||||||
$_SESSION['username'] = $user['username'];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function require_login(): void {
|
|
||||||
if (empty($_SESSION['user_id'])) {
|
|
||||||
header('Location: ' . url_for('login.php'));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function logout(): void {
|
|
||||||
$_SESSION = [];
|
|
||||||
if (ini_get("session.use_cookies")) {
|
|
||||||
$params = session_get_cookie_params();
|
|
||||||
setcookie(session_name(), '', time() - 42000,
|
|
||||||
$params["path"], $params["domain"],
|
|
||||||
$params["secure"], $params["httponly"]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
session_destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_logged_in_user(): ?array {
|
|
||||||
if (empty($_SESSION['user_id'])) return null;
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->prepare('SELECT id, username FROM users WHERE id = :id');
|
|
||||||
$stmt->execute([':id' => $_SESSION['user_id']]);
|
|
||||||
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_username(int $user_id, string $new_username): bool {
|
|
||||||
$pdo = get_db();
|
|
||||||
// Prüfen ob Username schon vergeben
|
|
||||||
$stmt = $pdo->prepare('SELECT id FROM users WHERE username = :u AND id != :id');
|
|
||||||
$stmt->execute([':u' => $new_username, ':id' => $user_id]);
|
|
||||||
if ($stmt->fetch()) {
|
|
||||||
return false; // Username existiert bereits
|
|
||||||
}
|
|
||||||
$stmt = $pdo->prepare('UPDATE users SET username = :u WHERE id = :id');
|
|
||||||
$stmt->execute([':u' => $new_username, ':id' => $user_id]);
|
|
||||||
$_SESSION['username'] = $new_username;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_password(int $user_id, string $current_password, string $new_password): bool {
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->prepare('SELECT password_hash FROM users WHERE id = :id');
|
|
||||||
$stmt->execute([':id' => $user_id]);
|
|
||||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
if (!$user || !password_verify($current_password, $user['password_hash'])) {
|
|
||||||
return false; // Aktuelles Passwort falsch
|
|
||||||
}
|
|
||||||
$new_hash = password_hash($new_password, PASSWORD_DEFAULT);
|
|
||||||
$stmt = $pdo->prepare('UPDATE users SET password_hash = :h WHERE id = :id');
|
|
||||||
$stmt->execute([':h' => $new_hash, ':id' => $user_id]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Grundkonfiguration für PIRP
|
|
||||||
|
|
||||||
// Datenbank-Parameter (per Env-Variable oder Fallback)
|
|
||||||
define('DB_HOST', getenv('DB_HOST') ?: '127.0.0.1');
|
|
||||||
define('DB_PORT', getenv('DB_PORT') ?: '5432');
|
|
||||||
define('DB_NAME', getenv('DB_NAME') ?: 'pirp');
|
|
||||||
define('DB_USER', getenv('DB_USER') ?: 'pirp_user');
|
|
||||||
define('DB_PASS', getenv('DB_PASS') ?: 'PIRPdb2025!');
|
|
||||||
|
|
||||||
// BASE_URL: wenn PIRP direkt unter http://server/ läuft -> leer
|
|
||||||
// wenn unter Unterverzeichnis, z.B. /pirp, dann BASE_URL auf '/pirp' setzen
|
|
||||||
define('BASE_URL', '');
|
|
||||||
|
|
||||||
// Fehleranzeige (DEV_MODE=1 zeigt Fehler im Browser)
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
ini_set('display_errors', getenv('DEV_MODE') ? '1' : '0');
|
|
||||||
ini_set('log_errors', '1');
|
|
||||||
|
|
||||||
if (session_status() === PHP_SESSION_NONE) {
|
|
||||||
session_start();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helfer für URLs
|
|
||||||
function url_for(string $path): string {
|
|
||||||
$base = rtrim(BASE_URL, '/');
|
|
||||||
$path = '/' . ltrim($path, '/');
|
|
||||||
return $base . $path;
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
<?php
|
|
||||||
// src/customer_functions.php
|
|
||||||
require_once __DIR__ . '/db.php';
|
|
||||||
|
|
||||||
function generate_customer_number(): string {
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->query("SELECT customer_number FROM customers WHERE customer_number LIKE 'PIKN-%' ORDER BY customer_number DESC LIMIT 1");
|
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
if ($row && preg_match('/^PIKN-(\d{6})$/', $row['customer_number'], $m)) {
|
|
||||||
$next = (int)$m[1] + 1;
|
|
||||||
} else {
|
|
||||||
$next = 1;
|
|
||||||
}
|
|
||||||
return sprintf('PIKN-%06d', $next);
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_customers(): array {
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->query('SELECT * FROM customers ORDER BY name');
|
|
||||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_customer(int $id): ?array {
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->prepare('SELECT * FROM customers WHERE id = :id');
|
|
||||||
$stmt->execute([':id' => $id]);
|
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
return $row ?: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function save_customer(?int $id, array $data): void {
|
|
||||||
$pdo = get_db();
|
|
||||||
if ($id) {
|
|
||||||
$sql = 'UPDATE customers SET name=:n, address=:a, zip=:z, city=:c, country=:co WHERE id=:id';
|
|
||||||
$pdo->prepare($sql)->execute([
|
|
||||||
':n' => $data['name'] ?? '',
|
|
||||||
':a' => $data['address'] ?? '',
|
|
||||||
':z' => $data['zip'] ?? '',
|
|
||||||
':c' => $data['city'] ?? '',
|
|
||||||
':co'=> $data['country'] ?? '',
|
|
||||||
':id'=> $id,
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
$sql = 'INSERT INTO customers (name, address, zip, city, country, customer_number)
|
|
||||||
VALUES (:n,:a,:z,:c,:co,:cn)';
|
|
||||||
$pdo->prepare($sql)->execute([
|
|
||||||
':n' => $data['name'] ?? '',
|
|
||||||
':a' => $data['address'] ?? '',
|
|
||||||
':z' => $data['zip'] ?? '',
|
|
||||||
':c' => $data['city'] ?? '',
|
|
||||||
':co' => $data['country'] ?? '',
|
|
||||||
':cn' => generate_customer_number(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function delete_customer(int $id): void {
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->prepare('DELETE FROM customers WHERE id = :id');
|
|
||||||
$stmt->execute([':id' => $id]);
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/config.php';
|
|
||||||
|
|
||||||
function get_db(): PDO {
|
|
||||||
static $pdo = null;
|
|
||||||
if ($pdo === null) {
|
|
||||||
$dsn = 'pgsql:host=' . DB_HOST . ';port=' . DB_PORT . ';dbname=' . DB_NAME . ';';
|
|
||||||
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
|
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
return $pdo;
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
<?php
|
|
||||||
// src/expense_functions.php
|
|
||||||
require_once __DIR__ . '/db.php';
|
|
||||||
|
|
||||||
function get_expenses(array $filters = []): array {
|
|
||||||
$pdo = get_db();
|
|
||||||
$sql = "SELECT * FROM expenses WHERE 1=1";
|
|
||||||
$params = [];
|
|
||||||
|
|
||||||
if (!empty($filters['from'])) {
|
|
||||||
$sql .= " AND expense_date >= :from";
|
|
||||||
$params[':from'] = $filters['from'];
|
|
||||||
}
|
|
||||||
if (!empty($filters['to'])) {
|
|
||||||
$sql .= " AND expense_date <= :to";
|
|
||||||
$params[':to'] = $filters['to'];
|
|
||||||
}
|
|
||||||
if (isset($filters['paid']) && $filters['paid'] !== '') {
|
|
||||||
$sql .= " AND paid = :paid";
|
|
||||||
$params[':paid'] = (bool)$filters['paid'];
|
|
||||||
}
|
|
||||||
if (!empty($filters['search'])) {
|
|
||||||
$sql .= " AND (description ILIKE :search OR category ILIKE :search)";
|
|
||||||
$params[':search'] = '%' . $filters['search'] . '%';
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql .= " ORDER BY expense_date DESC, id DESC";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_expense(int $id): ?array {
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->prepare("SELECT * FROM expenses WHERE id = :id");
|
|
||||||
$stmt->execute([':id' => $id]);
|
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
return $row ?: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Speichert eine Ausgabe (mit MwSt-Feldern und Aufwandskategorie).
|
|
||||||
* Gibt die ID der Ausgabe zurück (neu oder aktualisiert).
|
|
||||||
*/
|
|
||||||
function save_expense(?int $id, array $data): int {
|
|
||||||
$pdo = get_db();
|
|
||||||
$paid = !empty($data['paid']);
|
|
||||||
|
|
||||||
// MwSt-Berechnung: Brutto → Netto + VorSt
|
|
||||||
$amount = (float)$data['amount'];
|
|
||||||
$vat_rate = (float)($data['vat_rate'] ?? 0);
|
|
||||||
if ($vat_rate > 0) {
|
|
||||||
$total_net = round($amount / (1 + $vat_rate / 100), 2);
|
|
||||||
$total_vat = round($amount - $total_net, 2);
|
|
||||||
} else {
|
|
||||||
$total_net = $amount;
|
|
||||||
$total_vat = 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$expense_category_id = !empty($data['expense_category_id']) ? (int)$data['expense_category_id'] : null;
|
|
||||||
$payment_date = $paid ? ($data['payment_date'] ?? $data['expense_date']) : null;
|
|
||||||
|
|
||||||
if ($id) {
|
|
||||||
$sql = "UPDATE expenses
|
|
||||||
SET expense_date = :d,
|
|
||||||
description = :desc,
|
|
||||||
category = :cat,
|
|
||||||
amount = :amt,
|
|
||||||
vat_rate = :vr,
|
|
||||||
total_net = :tn,
|
|
||||||
total_vat = :tv,
|
|
||||||
expense_category_id = :ecid,
|
|
||||||
paid = :paid,
|
|
||||||
payment_date = :pd
|
|
||||||
WHERE id = :id";
|
|
||||||
$pdo->prepare($sql)->execute([
|
|
||||||
':d' => $data['expense_date'],
|
|
||||||
':desc' => $data['description'],
|
|
||||||
':cat' => $data['category'],
|
|
||||||
':amt' => $amount,
|
|
||||||
':vr' => $vat_rate,
|
|
||||||
':tn' => $total_net,
|
|
||||||
':tv' => $total_vat,
|
|
||||||
':ecid' => $expense_category_id,
|
|
||||||
':paid' => $paid ? 't' : 'f',
|
|
||||||
':pd' => $payment_date,
|
|
||||||
':id' => $id,
|
|
||||||
]);
|
|
||||||
return $id;
|
|
||||||
} else {
|
|
||||||
$sql = "INSERT INTO expenses
|
|
||||||
(expense_date, description, category, amount, vat_rate, total_net, total_vat, expense_category_id, paid, payment_date)
|
|
||||||
VALUES (:d, :desc, :cat, :amt, :vr, :tn, :tv, :ecid, :paid, :pd)
|
|
||||||
RETURNING id";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
':d' => $data['expense_date'],
|
|
||||||
':desc' => $data['description'],
|
|
||||||
':cat' => $data['category'],
|
|
||||||
':amt' => $amount,
|
|
||||||
':vr' => $vat_rate,
|
|
||||||
':tn' => $total_net,
|
|
||||||
':tv' => $total_vat,
|
|
||||||
':ecid' => $expense_category_id,
|
|
||||||
':paid' => $paid ? 't' : 'f',
|
|
||||||
':pd' => $payment_date,
|
|
||||||
]);
|
|
||||||
return (int)$stmt->fetchColumn();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function delete_expense(int $id): void {
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM expenses WHERE id = :id");
|
|
||||||
$stmt->execute([':id' => $id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Monochrome SVG icons for navigation (Hammer Editor style)
|
|
||||||
// Simple, geometric, 16x16 viewBox
|
|
||||||
|
|
||||||
function icon_dashboard(): string {
|
|
||||||
return '<svg viewBox="0 0 16 16"><path d="M1 1h6v6H1zM9 1h6v6H9zM1 9h6v6H1zM9 9h6v6H9z" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function icon_invoices(): string {
|
|
||||||
return '<svg viewBox="0 0 16 16"><path d="M3 1h7l3 3v11H3z" fill="none" stroke="currentColor" stroke-width="1.3"/><path d="M5 6h6M5 8.5h6M5 11h4" stroke="currentColor" stroke-width="1" opacity=".6"/></svg>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function icon_journal(): string {
|
|
||||||
return '<svg viewBox="0 0 16 16"><path d="M2 2h12v12H2z" fill="none" stroke="currentColor" stroke-width="1.3"/><path d="M2 5h12M2 8h12M2 11h12M6 2v12" stroke="currentColor" stroke-width=".8" opacity=".5"/></svg>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function icon_settings(): string {
|
|
||||||
return '<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="2.5" fill="none" stroke="currentColor" stroke-width="1.3"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2M3.05 3.05l1.4 1.4M11.55 11.55l1.4 1.4M3.05 12.95l1.4-1.4M11.55 4.45l1.4-1.4" stroke="currentColor" stroke-width="1.2"/></svg>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function icon_euer(): string {
|
|
||||||
return '<svg viewBox="0 0 16 16"><path d="M2 14V2h4l2 3h6v9H2z" fill="none" stroke="currentColor" stroke-width="1.3"/><path d="M5 7h6M5 10h4" stroke="currentColor" stroke-width="1" opacity=".6"/></svg>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function icon_customers(): string {
|
|
||||||
return '<svg viewBox="0 0 16 16"><circle cx="8" cy="5" r="2.5" fill="none" stroke="currentColor" stroke-width="1.3"/><path d="M3 14c0-3 2.2-5 5-5s5 2 5 5" fill="none" stroke="currentColor" stroke-width="1.3"/></svg>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function icon_expenses(): string {
|
|
||||||
return '<svg viewBox="0 0 16 16"><path d="M2 3h12v10H2z" fill="none" stroke="currentColor" stroke-width="1.3"/><path d="M5 8h6M8 5v6" stroke="currentColor" stroke-width="1" opacity=".6"/><path d="M2 6h12" stroke="currentColor" stroke-width=".8" opacity=".5"/></svg>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function icon_logout(): string {
|
|
||||||
return '<svg viewBox="0 0 16 16"><path d="M6 2H3v12h3M6 8h8M11 5l3 3-3 3" fill="none" stroke="currentColor" stroke-width="1.3"/></svg>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function icon_archive(): string {
|
|
||||||
return '<svg viewBox="0 0 16 16"><path d="M1 3h14v3H1z" fill="none" stroke="currentColor" stroke-width="1.3"/><path d="M2 6v8h12V6" fill="none" stroke="currentColor" stroke-width="1.3"/><path d="M6 9.5h4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>';
|
|
||||||
}
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
<?php
|
|
||||||
// src/invoice_functions.php
|
|
||||||
require_once __DIR__ . '/db.php';
|
|
||||||
|
|
||||||
function get_settings(): array {
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->query('SELECT * FROM settings ORDER BY id LIMIT 1');
|
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
if (!$row) {
|
|
||||||
// Default-Einstellungen
|
|
||||||
return [
|
|
||||||
'company_name' => 'Meine Firma',
|
|
||||||
'company_address' => '',
|
|
||||||
'company_zip' => '',
|
|
||||||
'company_city' => '',
|
|
||||||
'company_country' => '',
|
|
||||||
'tax_id' => '',
|
|
||||||
'vat_mode' => 'klein',
|
|
||||||
'default_vat_rate' => 19.00,
|
|
||||||
'payment_terms' => 'Zahlbar innerhalb von 14 Tagen.',
|
|
||||||
'footer_text' => '',
|
|
||||||
'logo_path' => '',
|
|
||||||
'iban' => '',
|
|
||||||
'phone' => '',
|
|
||||||
'email' => '',
|
|
||||||
'website' => '',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return $row;
|
|
||||||
}
|
|
||||||
|
|
||||||
function save_settings(array $data): void {
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->query('SELECT id FROM settings ORDER BY id LIMIT 1');
|
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
if ($row) {
|
|
||||||
$sql = 'UPDATE settings SET company_name=:cn, company_address=:ca, company_zip=:cz,
|
|
||||||
company_city=:cc, company_country=:ccountry, tax_id=:ti, vat_mode=:vm,
|
|
||||||
default_vat_rate=:vr, payment_terms=:pt, footer_text=:ft, logo_path=:lp,
|
|
||||||
iban=:iban, phone=:phone, email=:email, website=:website
|
|
||||||
WHERE id=:id';
|
|
||||||
$pdo->prepare($sql)->execute([
|
|
||||||
':cn' => $data['company_name'] ?? null,
|
|
||||||
':ca' => $data['company_address'] ?? null,
|
|
||||||
':cz' => $data['company_zip'] ?? null,
|
|
||||||
':cc' => $data['company_city'] ?? null,
|
|
||||||
':ccountry' => $data['company_country'] ?? null,
|
|
||||||
':ti' => $data['tax_id'] ?? null,
|
|
||||||
':vm' => $data['vat_mode'] ?? 'klein',
|
|
||||||
':vr' => $data['default_vat_rate'] ?? 19.0,
|
|
||||||
':pt' => $data['payment_terms'] ?? null,
|
|
||||||
':ft' => $data['footer_text'] ?? null,
|
|
||||||
':lp' => $data['logo_path'] ?? null,
|
|
||||||
':iban' => $data['iban'] ?? null,
|
|
||||||
':phone' => $data['phone'] ?? null,
|
|
||||||
':email' => $data['email'] ?? null,
|
|
||||||
':website' => $data['website'] ?? null,
|
|
||||||
':id' => $row['id'],
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
$sql = 'INSERT INTO settings (company_name, company_address, company_zip,
|
|
||||||
company_city, company_country, tax_id, vat_mode,
|
|
||||||
default_vat_rate, payment_terms, footer_text, logo_path,
|
|
||||||
iban, phone, email, website)
|
|
||||||
VALUES (:cn,:ca,:cz,:cc,:ccountry,:ti,:vm,:vr,:pt,:ft,:lp,:iban,:phone,:email,:website)';
|
|
||||||
$pdo->prepare($sql)->execute([
|
|
||||||
':cn' => $data['company_name'] ?? null,
|
|
||||||
':ca' => $data['company_address'] ?? null,
|
|
||||||
':cz' => $data['company_zip'] ?? null,
|
|
||||||
':cc' => $data['company_city'] ?? null,
|
|
||||||
':ccountry' => $data['company_country'] ?? null,
|
|
||||||
':ti' => $data['tax_id'] ?? null,
|
|
||||||
':vm' => $data['vat_mode'] ?? 'klein',
|
|
||||||
':vr' => $data['default_vat_rate'] ?? 19.0,
|
|
||||||
':pt' => $data['payment_terms'] ?? null,
|
|
||||||
':ft' => $data['footer_text'] ?? null,
|
|
||||||
':lp' => $data['logo_path'] ?? null,
|
|
||||||
':iban' => $data['iban'] ?? null,
|
|
||||||
':phone' => $data['phone'] ?? null,
|
|
||||||
':email' => $data['email'] ?? null,
|
|
||||||
':website' => $data['website'] ?? null,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function generate_invoice_number(): string {
|
|
||||||
$year = date('Y');
|
|
||||||
$pdo = get_db();
|
|
||||||
$prefix = "PIRP-$year-";
|
|
||||||
$stmt = $pdo->prepare("SELECT invoice_number FROM invoices
|
|
||||||
WHERE invoice_number LIKE :prefix
|
|
||||||
ORDER BY invoice_number DESC
|
|
||||||
LIMIT 1");
|
|
||||||
$stmt->execute([':prefix' => $prefix . '%']);
|
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
if ($row) {
|
|
||||||
$last = $row['invoice_number'];
|
|
||||||
$numPart = (int)substr($last, -5);
|
|
||||||
$next = $numPart + 1;
|
|
||||||
} else {
|
|
||||||
$next = 1;
|
|
||||||
}
|
|
||||||
return sprintf("PIRP-%s-%05d", $year, $next);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lädt eine Rechnung mit Kundendaten.
|
|
||||||
*/
|
|
||||||
function get_invoice_with_customer(int $id): ?array {
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->prepare("SELECT i.*, c.name AS customer_name, c.address AS customer_address,
|
|
||||||
c.zip AS customer_zip, c.city AS customer_city,
|
|
||||||
c.country AS customer_country, c.customer_number
|
|
||||||
FROM invoices i
|
|
||||||
JOIN customers c ON c.id = i.customer_id
|
|
||||||
WHERE i.id = :id");
|
|
||||||
$stmt->execute([':id' => $id]);
|
|
||||||
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Erstellt eine Stornorechnung zu einer bestehenden Rechnung.
|
|
||||||
* Gibt die ID der neuen Stornorechnung zurück.
|
|
||||||
*/
|
|
||||||
function create_storno_invoice(int $original_id): int {
|
|
||||||
$pdo = get_db();
|
|
||||||
|
|
||||||
$inv = get_invoice_with_customer($original_id);
|
|
||||||
if (!$inv) throw new \RuntimeException('Rechnung nicht gefunden');
|
|
||||||
|
|
||||||
// Positionen laden
|
|
||||||
$stmt = $pdo->prepare("SELECT * FROM invoice_items WHERE invoice_id = :id ORDER BY position_no");
|
|
||||||
$stmt->execute([':id' => $original_id]);
|
|
||||||
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
$storno_number = generate_invoice_number();
|
|
||||||
$today = date('Y-m-d');
|
|
||||||
|
|
||||||
$pdo->beginTransaction();
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO invoices
|
|
||||||
(invoice_number, customer_id, invoice_date, service_date,
|
|
||||||
vat_mode, vat_rate, payment_terms, notes_internal,
|
|
||||||
total_net, total_vat, total_gross,
|
|
||||||
paid, payment_date, is_storno, storno_of)
|
|
||||||
VALUES (:num, :cid, :idate, :sdate,
|
|
||||||
:vat_mode, :vat_rate, :pt, :notes,
|
|
||||||
:total_net, :total_vat, :total_gross,
|
|
||||||
TRUE, :pdate, TRUE, :storno_of)
|
|
||||||
RETURNING id");
|
|
||||||
$stmt->execute([
|
|
||||||
':num' => $storno_number,
|
|
||||||
':cid' => $inv['customer_id'],
|
|
||||||
':idate' => $today,
|
|
||||||
':sdate' => $inv['service_date'],
|
|
||||||
':vat_mode' => $inv['vat_mode'],
|
|
||||||
':vat_rate' => $inv['vat_rate'],
|
|
||||||
':pt' => $inv['payment_terms'],
|
|
||||||
':notes' => 'Storno von ' . $inv['invoice_number'],
|
|
||||||
':total_net' => -(float)$inv['total_net'],
|
|
||||||
':total_vat' => -(float)$inv['total_vat'],
|
|
||||||
':total_gross' => -(float)$inv['total_gross'],
|
|
||||||
':pdate' => $today,
|
|
||||||
':storno_of' => $original_id,
|
|
||||||
]);
|
|
||||||
$storno_id = (int)$stmt->fetchColumn();
|
|
||||||
|
|
||||||
// Positionen mit negativen Mengen
|
|
||||||
foreach ($items as $pos) {
|
|
||||||
$pdo->prepare("INSERT INTO invoice_items
|
|
||||||
(invoice_id, position_no, description, quantity, unit_price, vat_rate)
|
|
||||||
VALUES (:iid, :pos, :desc, :qty, :price, :vr)")
|
|
||||||
->execute([
|
|
||||||
':iid' => $storno_id,
|
|
||||||
':pos' => $pos['position_no'],
|
|
||||||
':desc' => $pos['description'],
|
|
||||||
':qty' => -(float)$pos['quantity'],
|
|
||||||
':price' => (float)$pos['unit_price'],
|
|
||||||
':vr' => (float)$pos['vat_rate'],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo->commit();
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$pdo->rollBack();
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $storno_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Erstellt eine stornierte Journalbuchung (Umkehr der Originalkonten).
|
|
||||||
* Nur aufrufen wenn die originale Rechnung bereits gebucht war.
|
|
||||||
*/
|
|
||||||
function create_storno_journal_entry(int $original_invoice_id, int $storno_invoice_id): int {
|
|
||||||
$pdo = get_db();
|
|
||||||
require_once __DIR__ . '/journal_functions.php';
|
|
||||||
|
|
||||||
// Originaljournal laden
|
|
||||||
$orig_entry = get_journal_entry_for_invoice($original_invoice_id);
|
|
||||||
if (!$orig_entry) throw new \RuntimeException('Keine Journalbuchung für die Originalrechnung gefunden');
|
|
||||||
|
|
||||||
// Konten des Originaleintrags laden
|
|
||||||
$stmt = $pdo->prepare("SELECT * FROM journal_entry_accounts WHERE entry_id = :id ORDER BY id");
|
|
||||||
$stmt->execute([':id' => $orig_entry['id']]);
|
|
||||||
$orig_accounts = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
// Storno-Rechnung laden
|
|
||||||
$storno_inv = get_invoice_with_customer($storno_invoice_id);
|
|
||||||
|
|
||||||
$today = date('Y-m-d');
|
|
||||||
$booking_year = (int)date('Y');
|
|
||||||
$year_id = ensure_journal_year($booking_year);
|
|
||||||
|
|
||||||
$data = [
|
|
||||||
'year_id' => $year_id,
|
|
||||||
'entry_date' => $today,
|
|
||||||
'description' => 'Storno: ' . $orig_entry['description'],
|
|
||||||
'attachment_note' => $storno_inv['invoice_number'],
|
|
||||||
'amount' => abs((float)$orig_entry['amount']),
|
|
||||||
'supplier_id' => null,
|
|
||||||
'sort_order' => 0,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Konten mit getauschten Seiten
|
|
||||||
$accounts = [];
|
|
||||||
foreach ($orig_accounts as $acct) {
|
|
||||||
$accounts[] = [
|
|
||||||
'account_type' => $acct['account_type'],
|
|
||||||
'side' => $acct['side'] === 'soll' ? 'haben' : 'soll',
|
|
||||||
'amount' => (float)$acct['amount'],
|
|
||||||
'revenue_category_id' => $acct['revenue_category_id'],
|
|
||||||
'expense_category_id' => $acct['expense_category_id'],
|
|
||||||
'note' => $acct['note'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$entry_id = save_journal_entry(null, $data, $accounts);
|
|
||||||
|
|
||||||
$pdo->prepare('UPDATE journal_entries SET invoice_id = :iid, source_type = :st WHERE id = :eid')
|
|
||||||
->execute([':iid' => $storno_invoice_id, ':st' => 'invoice_payment', ':eid' => $entry_id]);
|
|
||||||
|
|
||||||
return $entry_id;
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,635 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* PDF-Archivierungsfunktionen für GoBD-konforme Rechnungsspeicherung
|
|
||||||
*
|
|
||||||
* In Deutschland müssen Rechnungen unveränderbar gespeichert werden (GoBD).
|
|
||||||
* Diese Funktionen generieren PDFs einmalig und speichern sie permanent.
|
|
||||||
*/
|
|
||||||
require_once __DIR__ . '/db.php';
|
|
||||||
require_once __DIR__ . '/invoice_functions.php';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generiert das HTML für eine Rechnung (Snapshot der Daten).
|
|
||||||
*/
|
|
||||||
function generate_invoice_html(array $invoice, array $items, array $settings): string {
|
|
||||||
// Logo als Base64
|
|
||||||
$logoDataUri = '';
|
|
||||||
if (!empty($settings['logo_path'])) {
|
|
||||||
$fsPath = dirname(__DIR__) . '/public/' . $settings['logo_path'];
|
|
||||||
if (is_readable($fsPath)) {
|
|
||||||
$imageData = file_get_contents($fsPath);
|
|
||||||
if ($imageData !== false) {
|
|
||||||
$logoDataUri = 'data:image/png;base64,' . base64_encode($imageData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ob_start();
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<style>
|
|
||||||
@page { margin: 1.8cm 2cm 3.8cm 2cm; }
|
|
||||||
body { font-family: DejaVu Sans, sans-serif; font-size: 8pt; }
|
|
||||||
.header-box { padding: 4px 0 8px 0; text-align: center; }
|
|
||||||
.header-logo { max-height: 1.3cm; }
|
|
||||||
.address-window { position: absolute; top: 3.5cm; left: 0; width: 9cm; }
|
|
||||||
.address-sender { font-size: 6pt; margin-bottom: 0.2cm; white-space: nowrap; }
|
|
||||||
.address-recipient { font-weight: bold; }
|
|
||||||
.invoice-info-box { margin-top: 5.6cm; border: 1px solid #000; padding: 10px; }
|
|
||||||
.items-box { margin-top: 10px; border: 1px solid #000; padding: 10px; }
|
|
||||||
table.items { width: 100%; border-collapse: collapse; }
|
|
||||||
table.items th, table.items td { border-bottom: 1px solid #ccc; padding: 4px; }
|
|
||||||
table.items th { text-align: left; }
|
|
||||||
.footer-box { position: fixed; bottom: 0.7cm; left: 0; right: 0; padding: 0; border: 1px solid #000; font-size: 7pt; }
|
|
||||||
.footer-table { width: 100%; border-collapse: collapse; }
|
|
||||||
.footer-table td { vertical-align: top; padding: 6px 8px; border: none; }
|
|
||||||
.footer-left { width: 50%; text-align: left; }
|
|
||||||
.footer-right { width: 50%; text-align: right; line-height: 1.3; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<!-- HEADER -->
|
|
||||||
<div class="header-box">
|
|
||||||
<?php if ($logoDataUri): ?>
|
|
||||||
<img src="<?= $logoDataUri ?>" class="header-logo">
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ADRESSEN -->
|
|
||||||
<div class="address-window">
|
|
||||||
<div class="address-sender">
|
|
||||||
<?= htmlspecialchars($settings['company_name']) ?>
|
|
||||||
· <?= htmlspecialchars($settings['company_address']) ?>
|
|
||||||
· <?= htmlspecialchars($settings['company_zip'] . ' ' . $settings['company_city']) ?>
|
|
||||||
</div>
|
|
||||||
<div class="address-recipient">
|
|
||||||
<?= htmlspecialchars($invoice['customer_name']) ?><br>
|
|
||||||
<?= nl2br(htmlspecialchars($invoice['customer_address'])) ?><br>
|
|
||||||
<?= htmlspecialchars($invoice['customer_zip'] . ' ' . $invoice['customer_city']) ?><br>
|
|
||||||
<?= htmlspecialchars($invoice['customer_country']) ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- RECHNUNGSINFO -->
|
|
||||||
<div class="invoice-info-box">
|
|
||||||
<?php if (!empty($invoice['is_storno'])): ?>
|
|
||||||
<p style="color:#cc2222;font-weight:bold;font-size:10pt;margin-bottom:4px;">STORNORECHNUNG</p>
|
|
||||||
<p style="font-size:7pt;margin-bottom:6px;">Storno von <?= htmlspecialchars($invoice['notes_internal'] ?? '') ?></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
<h2>Rechnung <?= htmlspecialchars($invoice['invoice_number']) ?></h2>
|
|
||||||
<?php if (!empty($invoice['customer_number'])): ?>
|
|
||||||
Kundennummer: <?= htmlspecialchars($invoice['customer_number']) ?><br>
|
|
||||||
<?php endif; ?>
|
|
||||||
Rechnungsdatum: <?= date('d.m.Y', strtotime($invoice['invoice_date'])) ?><br>
|
|
||||||
<?php if (!empty($invoice['service_date'])): ?>
|
|
||||||
Leistungsdatum: <?= date('d.m.Y', strtotime($invoice['service_date'])) ?><br>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- POSITIONEN -->
|
|
||||||
<div class="items-box">
|
|
||||||
<table class="items">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style="width:5%;">Pos.</th>
|
|
||||||
<th>Beschreibung</th>
|
|
||||||
<th style="width:10%;">Menge</th>
|
|
||||||
<th style="width:15%;">Einzelpreis</th>
|
|
||||||
<th style="width:15%;">Gesamt</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($items as $pos): ?>
|
|
||||||
<tr>
|
|
||||||
<td><?= (int)$pos['position_no'] ?></td>
|
|
||||||
<td><?= nl2br(htmlspecialchars($pos['description'])) ?></td>
|
|
||||||
<td><?= number_format($pos['quantity'], 2, ',', '.') ?></td>
|
|
||||||
<td><?= number_format($pos['unit_price'], 2, ',', '.') ?> €</td>
|
|
||||||
<td><?= number_format($pos['quantity'] * $pos['unit_price'], 2, ',', '.') ?> €</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td colspan="4" style="text-align:right;"><strong>Zwischensumme:</strong></td>
|
|
||||||
<td style="text-align:right;"><strong><?= number_format($invoice['total_net'], 2, ',', '.') ?> €</strong></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<?php if ($invoice['vat_mode'] === 'normal'): ?>
|
|
||||||
<tr>
|
|
||||||
<td colspan="4" style="text-align:right;">Umsatzsteuer (<?= number_format($invoice['vat_rate'], 2, ',', '.') ?> %):</td>
|
|
||||||
<td style="text-align:right;"><?= number_format($invoice['total_vat'], 2, ',', '.') ?> €</td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td colspan="4" style="text-align:right; font-size:9pt;"><strong>Gesamtbetrag:</strong></td>
|
|
||||||
<td style="text-align:right; font-size:9pt;"><strong><?= number_format($invoice['total_gross'], 2, ',', '.') ?> €</strong></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<?php if (!empty($settings['payment_terms']) || $invoice['vat_mode'] === 'klein'): ?>
|
|
||||||
<tr>
|
|
||||||
<td colspan="5" style="text-align:right;">
|
|
||||||
<?php if (!empty($settings['payment_terms'])): ?>
|
|
||||||
<?= nl2br(htmlspecialchars($settings['payment_terms'])) ?><br>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($invoice['vat_mode'] === 'klein'): ?>
|
|
||||||
Umsatzsteuerbefreit aufgrund Kleingewerbe
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- FUSSZEILE -->
|
|
||||||
<div class="footer-box">
|
|
||||||
<table class="footer-table">
|
|
||||||
<tr>
|
|
||||||
<td class="footer-left">
|
|
||||||
<strong><?= htmlspecialchars($settings['company_name']) ?></strong><br>
|
|
||||||
<?= nl2br(htmlspecialchars($settings['company_address'])) ?><br>
|
|
||||||
<?= htmlspecialchars($settings['company_zip'] . ' ' . $settings['company_city']) ?><br>
|
|
||||||
<?= htmlspecialchars($settings['company_country']) ?>
|
|
||||||
</td>
|
|
||||||
<td class="footer-right">
|
|
||||||
<?php if (!empty($settings['iban'])): ?>
|
|
||||||
IBAN: <?= htmlspecialchars($settings['iban']) ?><br>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php
|
|
||||||
$line2 = [];
|
|
||||||
if (!empty($settings['phone'])) $line2[] = 'Tel: ' . $settings['phone'];
|
|
||||||
if (!empty($settings['email'])) $line2[] = 'E-Mail: ' . $settings['email'];
|
|
||||||
if (!empty($line2)) echo htmlspecialchars(implode(' · ', $line2)) . '<br>';
|
|
||||||
?>
|
|
||||||
<?php
|
|
||||||
$line3 = [];
|
|
||||||
if (!empty($settings['website'])) $line3[] = 'Web: ' . $settings['website'];
|
|
||||||
if (!empty($settings['tax_id'])) $line3[] = 'StNr/USt: ' . $settings['tax_id'];
|
|
||||||
if (!empty($line3)) echo htmlspecialchars(implode(' · ', $line3));
|
|
||||||
?>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
<?php
|
|
||||||
return ob_get_clean();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generiert eine PDF aus HTML und gibt den Inhalt zurück.
|
|
||||||
*/
|
|
||||||
function generate_pdf_content(string $html): string {
|
|
||||||
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
|
||||||
|
|
||||||
$options = new \Dompdf\Options();
|
|
||||||
$options->set('isRemoteEnabled', true);
|
|
||||||
$dompdf = new \Dompdf\Dompdf($options);
|
|
||||||
$dompdf->loadHtml($html, 'UTF-8');
|
|
||||||
$dompdf->setPaper('A4', 'portrait');
|
|
||||||
$dompdf->render();
|
|
||||||
|
|
||||||
return $dompdf->output();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Speichert die PDF-Datei unveränderlich im Archiv.
|
|
||||||
* Gibt den relativen Pfad zurück oder false bei Fehler.
|
|
||||||
*/
|
|
||||||
function archive_invoice_pdf(int $invoice_id): string|false {
|
|
||||||
$pdo = get_db();
|
|
||||||
|
|
||||||
// Prüfe ob bereits archiviert
|
|
||||||
$stmt = $pdo->prepare("SELECT pdf_path FROM invoices WHERE id = :id");
|
|
||||||
$stmt->execute([':id' => $invoice_id]);
|
|
||||||
$existing = $stmt->fetchColumn();
|
|
||||||
|
|
||||||
if ($existing) {
|
|
||||||
// Bereits archiviert - nicht überschreiben!
|
|
||||||
return $existing;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rechnungsdaten laden
|
|
||||||
$stmt = $pdo->prepare("SELECT i.*,
|
|
||||||
c.name AS customer_name,
|
|
||||||
c.address AS customer_address,
|
|
||||||
c.zip AS customer_zip,
|
|
||||||
c.city AS customer_city,
|
|
||||||
c.country AS customer_country,
|
|
||||||
c.customer_number
|
|
||||||
FROM invoices i
|
|
||||||
JOIN customers c ON c.id = i.customer_id
|
|
||||||
WHERE i.id = :id");
|
|
||||||
$stmt->execute([':id' => $invoice_id]);
|
|
||||||
$invoice = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
if (!$invoice) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Positionen laden
|
|
||||||
$stmtItems = $pdo->prepare("SELECT * FROM invoice_items WHERE invoice_id = :id ORDER BY position_no");
|
|
||||||
$stmtItems->execute([':id' => $invoice_id]);
|
|
||||||
$items = $stmtItems->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
// Aktuelle Einstellungen für Snapshot
|
|
||||||
$settings = get_settings();
|
|
||||||
|
|
||||||
// PDF generieren
|
|
||||||
$html = generate_invoice_html($invoice, $items, $settings);
|
|
||||||
$pdfContent = generate_pdf_content($html);
|
|
||||||
|
|
||||||
// Dateipfad erstellen (nach Jahr organisiert)
|
|
||||||
$year = date('Y', strtotime($invoice['invoice_date']));
|
|
||||||
$uploadDir = dirname(__DIR__) . '/public/uploads/invoices/' . $year;
|
|
||||||
|
|
||||||
if (!is_dir($uploadDir)) {
|
|
||||||
mkdir($uploadDir, 0775, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sicherer Dateiname
|
|
||||||
$safeInvoiceNumber = preg_replace('/[^A-Za-z0-9\-]/', '_', $invoice['invoice_number']);
|
|
||||||
$filename = $safeInvoiceNumber . '.pdf';
|
|
||||||
$fullPath = $uploadDir . '/' . $filename;
|
|
||||||
$relativePath = 'uploads/invoices/' . $year . '/' . $filename;
|
|
||||||
|
|
||||||
// Prüfe ob Datei bereits existiert (z.B. von abgebrochener Migration)
|
|
||||||
if (file_exists($fullPath)) {
|
|
||||||
// Datei existiert - Hash berechnen und DB aktualisieren
|
|
||||||
$existingContent = file_get_contents($fullPath);
|
|
||||||
if ($existingContent !== false) {
|
|
||||||
$hash = hash('sha256', $existingContent);
|
|
||||||
$stmt = $pdo->prepare("UPDATE invoices
|
|
||||||
SET pdf_path = :path,
|
|
||||||
pdf_hash = :hash,
|
|
||||||
pdf_generated_at = NOW()
|
|
||||||
WHERE id = :id AND pdf_path IS NULL");
|
|
||||||
$stmt->execute([
|
|
||||||
':path' => $relativePath,
|
|
||||||
':hash' => $hash,
|
|
||||||
':id' => $invoice_id
|
|
||||||
]);
|
|
||||||
return $relativePath;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// PDF speichern
|
|
||||||
$bytesWritten = file_put_contents($fullPath, $pdfContent);
|
|
||||||
if ($bytesWritten === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schreibschutz setzen (chmod 444)
|
|
||||||
chmod($fullPath, 0444);
|
|
||||||
|
|
||||||
// Hash berechnen
|
|
||||||
$hash = hash('sha256', $pdfContent);
|
|
||||||
|
|
||||||
// Datenbank aktualisieren
|
|
||||||
$stmt = $pdo->prepare("UPDATE invoices
|
|
||||||
SET pdf_path = :path,
|
|
||||||
pdf_hash = :hash,
|
|
||||||
pdf_generated_at = NOW()
|
|
||||||
WHERE id = :id AND pdf_path IS NULL");
|
|
||||||
$stmt->execute([
|
|
||||||
':path' => $relativePath,
|
|
||||||
':hash' => $hash,
|
|
||||||
':id' => $invoice_id
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $relativePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prüft die Integrität einer archivierten PDF.
|
|
||||||
* Gibt true zurück wenn Hash übereinstimmt, false sonst.
|
|
||||||
*/
|
|
||||||
function verify_invoice_pdf(int $invoice_id): ?bool {
|
|
||||||
$pdo = get_db();
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare("SELECT pdf_path, pdf_hash FROM invoices WHERE id = :id");
|
|
||||||
$stmt->execute([':id' => $invoice_id]);
|
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
if (!$row || empty($row['pdf_path']) || empty($row['pdf_hash'])) {
|
|
||||||
return null; // Keine archivierte PDF
|
|
||||||
}
|
|
||||||
|
|
||||||
$fullPath = dirname(__DIR__) . '/public/' . $row['pdf_path'];
|
|
||||||
|
|
||||||
if (!file_exists($fullPath)) {
|
|
||||||
return false; // Datei fehlt
|
|
||||||
}
|
|
||||||
|
|
||||||
$currentHash = hash_file('sha256', $fullPath);
|
|
||||||
return $currentHash === $row['pdf_hash'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Liefert den Pfad zur archivierten PDF oder null.
|
|
||||||
*/
|
|
||||||
function get_archived_pdf_path(int $invoice_id): ?string {
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->prepare("SELECT pdf_path FROM invoices WHERE id = :id");
|
|
||||||
$stmt->execute([':id' => $invoice_id]);
|
|
||||||
$path = $stmt->fetchColumn();
|
|
||||||
return $path ?: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Zählt Rechnungen ohne archivierte PDF.
|
|
||||||
*/
|
|
||||||
function count_unarchived_invoices(): int {
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->query("SELECT COUNT(*) FROM invoices WHERE pdf_path IS NULL");
|
|
||||||
return (int)$stmt->fetchColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prüft den GoBD-Status aller archivierten PDFs.
|
|
||||||
* Gibt ein Array mit Statistiken und Problemen zurück.
|
|
||||||
*/
|
|
||||||
function check_pdf_integrity_status(): array {
|
|
||||||
$pdo = get_db();
|
|
||||||
|
|
||||||
$result = [
|
|
||||||
'total_invoices' => 0,
|
|
||||||
'archived' => 0,
|
|
||||||
'unarchived' => 0,
|
|
||||||
'valid' => 0,
|
|
||||||
'invalid' => 0,
|
|
||||||
'missing_files' => 0,
|
|
||||||
'problems' => [],
|
|
||||||
'migration_needed' => false
|
|
||||||
];
|
|
||||||
|
|
||||||
// Gesamtzahl
|
|
||||||
$stmt = $pdo->query("SELECT COUNT(*) FROM invoices");
|
|
||||||
$result['total_invoices'] = (int)$stmt->fetchColumn();
|
|
||||||
|
|
||||||
// Prüfe ob pdf_path Spalte existiert
|
|
||||||
try {
|
|
||||||
$pdo->query("SELECT pdf_path FROM invoices LIMIT 1");
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
// Spalte existiert nicht - Migration erforderlich
|
|
||||||
$result['unarchived'] = $result['total_invoices'];
|
|
||||||
$result['migration_needed'] = true;
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nicht archiviert
|
|
||||||
$stmt = $pdo->query("SELECT COUNT(*) FROM invoices WHERE pdf_path IS NULL");
|
|
||||||
$result['unarchived'] = (int)$stmt->fetchColumn();
|
|
||||||
|
|
||||||
// Archivierte PDFs prüfen
|
|
||||||
$stmt = $pdo->query("SELECT id, invoice_number, pdf_path, pdf_hash FROM invoices WHERE pdf_path IS NOT NULL");
|
|
||||||
$archived = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
$result['archived'] = count($archived);
|
|
||||||
|
|
||||||
foreach ($archived as $inv) {
|
|
||||||
$fullPath = dirname(__DIR__) . '/public/' . $inv['pdf_path'];
|
|
||||||
|
|
||||||
if (!file_exists($fullPath)) {
|
|
||||||
$result['missing_files']++;
|
|
||||||
$result['problems'][] = [
|
|
||||||
'type' => 'missing',
|
|
||||||
'invoice_number' => $inv['invoice_number'],
|
|
||||||
'id' => $inv['id'],
|
|
||||||
'message' => 'PDF-Datei fehlt'
|
|
||||||
];
|
|
||||||
} elseif (!empty($inv['pdf_hash'])) {
|
|
||||||
$currentHash = hash_file('sha256', $fullPath);
|
|
||||||
if ($currentHash !== $inv['pdf_hash']) {
|
|
||||||
$result['invalid']++;
|
|
||||||
$result['problems'][] = [
|
|
||||||
'type' => 'corrupted',
|
|
||||||
'invoice_number' => $inv['invoice_number'],
|
|
||||||
'id' => $inv['id'],
|
|
||||||
'message' => 'Hash-Prüfung fehlgeschlagen (Datei manipuliert?)'
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
$result['valid']++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Kein Hash vorhanden
|
|
||||||
$result['valid']++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generiert das HTML für eine Mahnung.
|
|
||||||
*/
|
|
||||||
function generate_mahnung_html(array $mahnung, array $invoice, array $settings): string {
|
|
||||||
$level_labels = [1 => 'MAHNUNG', 2 => '2. MAHNUNG', 3 => '3. MAHNUNG / LETZTE MAHNUNG'];
|
|
||||||
$level_label = $level_labels[$mahnung['level']] ?? 'MAHNUNG';
|
|
||||||
|
|
||||||
$logoDataUri = '';
|
|
||||||
if (!empty($settings['logo_path'])) {
|
|
||||||
$fsPath = dirname(__DIR__) . '/public/' . $settings['logo_path'];
|
|
||||||
if (is_readable($fsPath)) {
|
|
||||||
$imageData = file_get_contents($fsPath);
|
|
||||||
if ($imageData !== false) {
|
|
||||||
$logoDataUri = 'data:image/png;base64,' . base64_encode($imageData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$days_overdue = (int)ceil((strtotime($mahnung['mahnung_date']) - strtotime($invoice['invoice_date'])) / 86400);
|
|
||||||
$total_due = (float)$invoice['total_gross'] + (float)$mahnung['fee_amount'];
|
|
||||||
|
|
||||||
ob_start();
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<style>
|
|
||||||
@page { margin: 1.8cm 2cm 3.8cm 2cm; }
|
|
||||||
body { font-family: DejaVu Sans, sans-serif; font-size: 8pt; }
|
|
||||||
.header-box { padding: 4px 0 8px 0; text-align: center; }
|
|
||||||
.header-logo { max-height: 1.3cm; }
|
|
||||||
.address-window { position: absolute; top: 3.5cm; left: 0; width: 9cm; }
|
|
||||||
.address-sender { font-size: 6pt; margin-bottom: 0.2cm; white-space: nowrap; }
|
|
||||||
.address-recipient { font-weight: bold; }
|
|
||||||
.mahnung-info-box { margin-top: 5.6cm; padding: 10px; }
|
|
||||||
.mahnung-title { font-size: 14pt; font-weight: bold; color: #cc2222; margin-bottom: 8px; }
|
|
||||||
.details-box { margin-top: 10px; border: 1px solid #000; padding: 10px; }
|
|
||||||
table.details { width: 100%; border-collapse: collapse; }
|
|
||||||
table.details td { padding: 4px 6px; border-bottom: 1px solid #eee; }
|
|
||||||
.total-row td { font-weight: bold; font-size: 9pt; border-top: 2px solid #000; border-bottom: none; }
|
|
||||||
.footer-box { position: fixed; bottom: 0.7cm; left: 0; right: 0; padding: 0; border: 1px solid #000; font-size: 7pt; }
|
|
||||||
.footer-table { width: 100%; border-collapse: collapse; }
|
|
||||||
.footer-table td { vertical-align: top; padding: 6px 8px; }
|
|
||||||
.footer-left { width: 50%; }
|
|
||||||
.footer-right { width: 50%; text-align: right; line-height: 1.3; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="header-box">
|
|
||||||
<?php if ($logoDataUri): ?>
|
|
||||||
<img src="<?= $logoDataUri ?>" class="header-logo">
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="address-window">
|
|
||||||
<div class="address-sender">
|
|
||||||
<?= htmlspecialchars($settings['company_name']) ?>
|
|
||||||
· <?= htmlspecialchars($settings['company_address']) ?>
|
|
||||||
· <?= htmlspecialchars($settings['company_zip'] . ' ' . $settings['company_city']) ?>
|
|
||||||
</div>
|
|
||||||
<div class="address-recipient">
|
|
||||||
<?= htmlspecialchars($invoice['customer_name']) ?><br>
|
|
||||||
<?= nl2br(htmlspecialchars($invoice['customer_address'])) ?><br>
|
|
||||||
<?= htmlspecialchars($invoice['customer_zip'] . ' ' . $invoice['customer_city']) ?><br>
|
|
||||||
<?= htmlspecialchars($invoice['customer_country']) ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mahnung-info-box">
|
|
||||||
<div class="mahnung-title"><?= $level_label ?></div>
|
|
||||||
<p><?= htmlspecialchars($settings['company_city'] ?: $settings['company_city']) ?>, <?= date('d.m.Y', strtotime($mahnung['mahnung_date'])) ?></p>
|
|
||||||
<p style="margin-top:10px;">
|
|
||||||
Sehr geehrte Damen und Herren,<br><br>
|
|
||||||
trotz unserer Zahlungserinnerung ist der folgende Betrag noch nicht bei uns eingegangen.
|
|
||||||
Wir bitten Sie, die offene Forderung umgehend zu begleichen.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="details-box">
|
|
||||||
<table class="details">
|
|
||||||
<tr>
|
|
||||||
<td>Rechnungsnummer:</td>
|
|
||||||
<td><strong><?= htmlspecialchars($invoice['invoice_number']) ?></strong></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Rechnungsdatum:</td>
|
|
||||||
<td><?= date('d.m.Y', strtotime($invoice['invoice_date'])) ?></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Rechnungsbetrag:</td>
|
|
||||||
<td><?= number_format((float)$invoice['total_gross'], 2, ',', '.') ?> €</td>
|
|
||||||
</tr>
|
|
||||||
<?php if ((float)$mahnung['fee_amount'] > 0): ?>
|
|
||||||
<tr>
|
|
||||||
<td>Mahngebühr:</td>
|
|
||||||
<td><?= number_format((float)$mahnung['fee_amount'], 2, ',', '.') ?> €</td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
<tr class="total-row">
|
|
||||||
<td>Offener Betrag:</td>
|
|
||||||
<td><?= number_format($total_due, 2, ',', '.') ?> €</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<p style="margin-top:10px;">
|
|
||||||
Bitte überweisen Sie den Betrag von <strong><?= number_format($total_due, 2, ',', '.') ?> €</strong>
|
|
||||||
innerhalb von 7 Tagen auf unser Konto.
|
|
||||||
</p>
|
|
||||||
<?php if (!empty($settings['iban'])): ?>
|
|
||||||
<p style="font-size:7.5pt; margin-top:6px;">
|
|
||||||
IBAN: <?= htmlspecialchars($settings['iban']) ?>
|
|
||||||
· Verwendungszweck: <?= htmlspecialchars($invoice['invoice_number']) ?>
|
|
||||||
</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="footer-box">
|
|
||||||
<table class="footer-table">
|
|
||||||
<tr>
|
|
||||||
<td class="footer-left">
|
|
||||||
<strong><?= htmlspecialchars($settings['company_name']) ?></strong><br>
|
|
||||||
<?= nl2br(htmlspecialchars($settings['company_address'])) ?><br>
|
|
||||||
<?= htmlspecialchars($settings['company_zip'] . ' ' . $settings['company_city']) ?>
|
|
||||||
</td>
|
|
||||||
<td class="footer-right">
|
|
||||||
<?php if (!empty($settings['iban'])): ?>
|
|
||||||
IBAN: <?= htmlspecialchars($settings['iban']) ?><br>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php
|
|
||||||
$line2 = [];
|
|
||||||
if (!empty($settings['phone'])) $line2[] = 'Tel: ' . $settings['phone'];
|
|
||||||
if (!empty($settings['email'])) $line2[] = 'E-Mail: ' . $settings['email'];
|
|
||||||
if (!empty($line2)) echo htmlspecialchars(implode(' · ', $line2)) . '<br>';
|
|
||||||
?>
|
|
||||||
<?php if (!empty($settings['tax_id'])): ?>
|
|
||||||
StNr/USt: <?= htmlspecialchars($settings['tax_id']) ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
<?php
|
|
||||||
return ob_get_clean();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generiert + speichert eine Mahnung als PDF und aktualisiert die DB.
|
|
||||||
*/
|
|
||||||
function archive_mahnung_pdf(int $mahnung_id): string|false {
|
|
||||||
$pdo = get_db();
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare("SELECT m.*, i.invoice_number, i.invoice_date, i.total_gross,
|
|
||||||
i.customer_id, c.name AS customer_name,
|
|
||||||
c.address AS customer_address, c.zip AS customer_zip,
|
|
||||||
c.city AS customer_city, c.country AS customer_country
|
|
||||||
FROM mahnungen m
|
|
||||||
JOIN invoices i ON i.id = m.invoice_id
|
|
||||||
JOIN customers c ON c.id = i.customer_id
|
|
||||||
WHERE m.id = :id");
|
|
||||||
$stmt->execute([':id' => $mahnung_id]);
|
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
if (!$row) return false;
|
|
||||||
|
|
||||||
$mahnung = [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'level' => $row['level'],
|
|
||||||
'mahnung_date' => $row['mahnung_date'],
|
|
||||||
'fee_amount' => $row['fee_amount'],
|
|
||||||
];
|
|
||||||
$invoice = [
|
|
||||||
'invoice_number' => $row['invoice_number'],
|
|
||||||
'invoice_date' => $row['invoice_date'],
|
|
||||||
'total_gross' => $row['total_gross'],
|
|
||||||
'customer_name' => $row['customer_name'],
|
|
||||||
'customer_address' => $row['customer_address'],
|
|
||||||
'customer_zip' => $row['customer_zip'],
|
|
||||||
'customer_city' => $row['customer_city'],
|
|
||||||
'customer_country' => $row['customer_country'],
|
|
||||||
];
|
|
||||||
$settings = get_settings();
|
|
||||||
|
|
||||||
$html = generate_mahnung_html($mahnung, $invoice, $settings);
|
|
||||||
$pdfContent = generate_pdf_content($html);
|
|
||||||
|
|
||||||
$year = date('Y', strtotime($row['mahnung_date']));
|
|
||||||
$dir = dirname(__DIR__) . '/public/uploads/mahnungen/' . $year;
|
|
||||||
if (!is_dir($dir)) mkdir($dir, 0775, true);
|
|
||||||
|
|
||||||
$safe_num = preg_replace('/[^A-Za-z0-9\-]/', '_', $row['invoice_number']);
|
|
||||||
$filename = 'MAHNUNG-' . $safe_num . '-L' . $row['level'] . '.pdf';
|
|
||||||
$fullPath = $dir . '/' . $filename;
|
|
||||||
$relPath = 'uploads/mahnungen/' . $year . '/' . $filename;
|
|
||||||
|
|
||||||
if (file_put_contents($fullPath, $pdfContent) === false) return false;
|
|
||||||
chmod($fullPath, 0444);
|
|
||||||
|
|
||||||
$pdo->prepare("UPDATE mahnungen SET pdf_path = :p WHERE id = :id")
|
|
||||||
->execute([':p' => $relPath, ':id' => $mahnung_id]);
|
|
||||||
|
|
||||||
return $relPath;
|
|
||||||
}
|
|
||||||
@@ -1,327 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Funktionen für wiederkehrende Rechnungen (Abo-Rechnungen)
|
|
||||||
*/
|
|
||||||
require_once __DIR__ . '/db.php';
|
|
||||||
require_once __DIR__ . '/invoice_functions.php';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Speichert eine Abo-Vorlage (neu oder Update).
|
|
||||||
* Gibt die Template-ID zurück.
|
|
||||||
*/
|
|
||||||
function save_recurring_template(?int $id, array $data): int {
|
|
||||||
$pdo = get_db();
|
|
||||||
|
|
||||||
if ($id) {
|
|
||||||
$stmt = $pdo->prepare("UPDATE recurring_templates SET
|
|
||||||
template_name = :name,
|
|
||||||
customer_id = :cid,
|
|
||||||
interval_type = :interval,
|
|
||||||
start_date = :start,
|
|
||||||
end_date = :end,
|
|
||||||
next_due_date = :next,
|
|
||||||
vat_mode = :vm,
|
|
||||||
vat_rate = :vr,
|
|
||||||
is_active = :active,
|
|
||||||
notes_internal = :notes
|
|
||||||
WHERE id = :id");
|
|
||||||
$stmt->execute([
|
|
||||||
':name' => $data['template_name'],
|
|
||||||
':cid' => $data['customer_id'],
|
|
||||||
':interval' => $data['interval_type'],
|
|
||||||
':start' => $data['start_date'],
|
|
||||||
':end' => $data['end_date'] ?: null,
|
|
||||||
':next' => $data['next_due_date'],
|
|
||||||
':vm' => $data['vat_mode'],
|
|
||||||
':vr' => $data['vat_rate'],
|
|
||||||
':active' => $data['is_active'] ? 1 : 0,
|
|
||||||
':notes' => $data['notes_internal'] ?? null,
|
|
||||||
':id' => $id
|
|
||||||
]);
|
|
||||||
return $id;
|
|
||||||
} else {
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO recurring_templates
|
|
||||||
(template_name, customer_id, interval_type, start_date, end_date,
|
|
||||||
next_due_date, vat_mode, vat_rate, is_active, notes_internal)
|
|
||||||
VALUES (:name, :cid, :interval, :start, :end, :next, :vm, :vr, :active, :notes)
|
|
||||||
RETURNING id");
|
|
||||||
$stmt->execute([
|
|
||||||
':name' => $data['template_name'],
|
|
||||||
':cid' => $data['customer_id'],
|
|
||||||
':interval' => $data['interval_type'],
|
|
||||||
':start' => $data['start_date'],
|
|
||||||
':end' => $data['end_date'] ?: null,
|
|
||||||
':next' => $data['next_due_date'],
|
|
||||||
':vm' => $data['vat_mode'],
|
|
||||||
':vr' => $data['vat_rate'],
|
|
||||||
':active' => $data['is_active'] ? 1 : 0,
|
|
||||||
':notes' => $data['notes_internal'] ?? null
|
|
||||||
]);
|
|
||||||
return (int)$stmt->fetchColumn();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holt eine einzelne Abo-Vorlage.
|
|
||||||
*/
|
|
||||||
function get_recurring_template(int $id): ?array {
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->prepare("SELECT rt.*, c.name AS customer_name, c.customer_number
|
|
||||||
FROM recurring_templates rt
|
|
||||||
JOIN customers c ON c.id = rt.customer_id
|
|
||||||
WHERE rt.id = :id");
|
|
||||||
$stmt->execute([':id' => $id]);
|
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
return $row ?: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holt alle Abo-Vorlagen.
|
|
||||||
*/
|
|
||||||
function get_recurring_templates(bool $active_only = false): array {
|
|
||||||
$pdo = get_db();
|
|
||||||
$sql = "SELECT rt.*, c.name AS customer_name, c.customer_number
|
|
||||||
FROM recurring_templates rt
|
|
||||||
JOIN customers c ON c.id = rt.customer_id";
|
|
||||||
if ($active_only) {
|
|
||||||
$sql .= " WHERE rt.is_active = TRUE";
|
|
||||||
}
|
|
||||||
$sql .= " ORDER BY rt.next_due_date ASC";
|
|
||||||
$stmt = $pdo->query($sql);
|
|
||||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holt die Positionen einer Abo-Vorlage.
|
|
||||||
*/
|
|
||||||
function get_recurring_template_items(int $template_id): array {
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->prepare("SELECT * FROM recurring_template_items
|
|
||||||
WHERE template_id = :tid ORDER BY position_no");
|
|
||||||
$stmt->execute([':tid' => $template_id]);
|
|
||||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Speichert die Positionen einer Abo-Vorlage (ersetzt alle).
|
|
||||||
*/
|
|
||||||
function save_recurring_template_items(int $template_id, array $items): void {
|
|
||||||
$pdo = get_db();
|
|
||||||
|
|
||||||
// Alte löschen
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM recurring_template_items WHERE template_id = :tid");
|
|
||||||
$stmt->execute([':tid' => $template_id]);
|
|
||||||
|
|
||||||
// Neue einfügen
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO recurring_template_items
|
|
||||||
(template_id, position_no, description, quantity, unit_price)
|
|
||||||
VALUES (:tid, :pn, :desc, :qty, :price)");
|
|
||||||
|
|
||||||
foreach ($items as $item) {
|
|
||||||
$stmt->execute([
|
|
||||||
':tid' => $template_id,
|
|
||||||
':pn' => $item['position_no'],
|
|
||||||
':desc' => $item['description'],
|
|
||||||
':qty' => $item['quantity'],
|
|
||||||
':price' => $item['unit_price']
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Löscht eine Abo-Vorlage.
|
|
||||||
*/
|
|
||||||
function delete_recurring_template(int $id): void {
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM recurring_templates WHERE id = :id");
|
|
||||||
$stmt->execute([':id' => $id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Berechnet das nächste Fälligkeitsdatum.
|
|
||||||
*/
|
|
||||||
function calculate_next_due_date(string $interval_type, string $current_date): string {
|
|
||||||
$date = new DateTime($current_date);
|
|
||||||
|
|
||||||
switch ($interval_type) {
|
|
||||||
case 'monthly':
|
|
||||||
$date->modify('+1 month');
|
|
||||||
break;
|
|
||||||
case 'quarterly':
|
|
||||||
$date->modify('+3 months');
|
|
||||||
break;
|
|
||||||
case 'yearly':
|
|
||||||
$date->modify('+1 year');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $date->format('Y-m-d');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holt alle fälligen Abo-Rechnungen.
|
|
||||||
*/
|
|
||||||
function get_pending_recurring_invoices(): array {
|
|
||||||
$pdo = get_db();
|
|
||||||
$today = date('Y-m-d');
|
|
||||||
$stmt = $pdo->prepare("SELECT rt.*, c.name AS customer_name, c.customer_number
|
|
||||||
FROM recurring_templates rt
|
|
||||||
JOIN customers c ON c.id = rt.customer_id
|
|
||||||
WHERE rt.is_active = TRUE
|
|
||||||
AND rt.next_due_date <= :today
|
|
||||||
AND (rt.end_date IS NULL OR rt.end_date >= :today)
|
|
||||||
ORDER BY rt.next_due_date ASC");
|
|
||||||
$stmt->execute([':today' => $today]);
|
|
||||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Zählt fällige Abo-Rechnungen.
|
|
||||||
*/
|
|
||||||
function count_pending_recurring_invoices(): int {
|
|
||||||
$pdo = get_db();
|
|
||||||
$today = date('Y-m-d');
|
|
||||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM recurring_templates
|
|
||||||
WHERE is_active = TRUE
|
|
||||||
AND next_due_date <= :today
|
|
||||||
AND (end_date IS NULL OR end_date >= :today)");
|
|
||||||
$stmt->execute([':today' => $today]);
|
|
||||||
return (int)$stmt->fetchColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generiert eine Rechnung aus einer Abo-Vorlage.
|
|
||||||
* Gibt die neue Rechnungs-ID zurück oder false bei Fehler.
|
|
||||||
*/
|
|
||||||
function generate_invoice_from_template(int $template_id): int|false {
|
|
||||||
$pdo = get_db();
|
|
||||||
|
|
||||||
$template = get_recurring_template($template_id);
|
|
||||||
if (!$template || !$template['is_active']) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$items = get_recurring_template_items($template_id);
|
|
||||||
if (empty($items)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Summen berechnen
|
|
||||||
$total_net = 0.0;
|
|
||||||
foreach ($items as $item) {
|
|
||||||
$total_net += $item['quantity'] * $item['unit_price'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$vat_mode = $template['vat_mode'];
|
|
||||||
$vat_rate = (float)$template['vat_rate'];
|
|
||||||
|
|
||||||
if ($vat_mode === 'normal') {
|
|
||||||
$total_vat = round($total_net * $vat_rate / 100, 2);
|
|
||||||
} else {
|
|
||||||
$total_vat = 0.0;
|
|
||||||
}
|
|
||||||
$total_gross = $total_net + $total_vat;
|
|
||||||
|
|
||||||
$settings = get_settings();
|
|
||||||
|
|
||||||
$pdo->beginTransaction();
|
|
||||||
try {
|
|
||||||
// Rechnung erstellen
|
|
||||||
$invoice_number = generate_invoice_number();
|
|
||||||
$invoice_date = date('Y-m-d');
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO invoices
|
|
||||||
(invoice_number, customer_id, invoice_date, service_date, vat_mode, vat_rate,
|
|
||||||
payment_terms, notes_internal, total_net, total_vat, total_gross, paid)
|
|
||||||
VALUES (:in, :cid, :idate, :sdate, :vm, :vr, :pt, :ni, :tn, :tv, :tg, FALSE)
|
|
||||||
RETURNING id");
|
|
||||||
$stmt->execute([
|
|
||||||
':in' => $invoice_number,
|
|
||||||
':cid' => $template['customer_id'],
|
|
||||||
':idate' => $invoice_date,
|
|
||||||
':sdate' => $invoice_date,
|
|
||||||
':vm' => $vat_mode,
|
|
||||||
':vr' => $vat_rate,
|
|
||||||
':pt' => $settings['payment_terms'] ?? null,
|
|
||||||
':ni' => 'Generiert aus Abo-Vorlage: ' . $template['template_name'],
|
|
||||||
':tn' => $total_net,
|
|
||||||
':tv' => $total_vat,
|
|
||||||
':tg' => $total_gross
|
|
||||||
]);
|
|
||||||
$invoice_id = (int)$stmt->fetchColumn();
|
|
||||||
|
|
||||||
// Positionen einfügen
|
|
||||||
$stmtItem = $pdo->prepare("INSERT INTO invoice_items
|
|
||||||
(invoice_id, position_no, description, quantity, unit_price, vat_rate)
|
|
||||||
VALUES (:iid, :pn, :d, :q, :up, :vr)");
|
|
||||||
|
|
||||||
foreach ($items as $item) {
|
|
||||||
$stmtItem->execute([
|
|
||||||
':iid' => $invoice_id,
|
|
||||||
':pn' => $item['position_no'],
|
|
||||||
':d' => $item['description'],
|
|
||||||
':q' => $item['quantity'],
|
|
||||||
':up' => $item['unit_price'],
|
|
||||||
':vr' => $vat_rate
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log-Eintrag
|
|
||||||
$stmtLog = $pdo->prepare("INSERT INTO recurring_log
|
|
||||||
(template_id, invoice_id, due_date, status)
|
|
||||||
VALUES (:tid, :iid, :due, 'generated')");
|
|
||||||
$stmtLog->execute([
|
|
||||||
':tid' => $template_id,
|
|
||||||
':iid' => $invoice_id,
|
|
||||||
':due' => $template['next_due_date']
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Nächstes Fälligkeitsdatum setzen
|
|
||||||
$next_due = calculate_next_due_date($template['interval_type'], $template['next_due_date']);
|
|
||||||
$stmtUpdate = $pdo->prepare("UPDATE recurring_templates
|
|
||||||
SET next_due_date = :next WHERE id = :id");
|
|
||||||
$stmtUpdate->execute([':next' => $next_due, ':id' => $template_id]);
|
|
||||||
|
|
||||||
$pdo->commit();
|
|
||||||
|
|
||||||
// PDF archivieren
|
|
||||||
require_once __DIR__ . '/pdf_functions.php';
|
|
||||||
archive_invoice_pdf($invoice_id);
|
|
||||||
|
|
||||||
return $invoice_id;
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$pdo->rollBack();
|
|
||||||
error_log("Fehler bei Rechnungsgenerierung aus Vorlage $template_id: " . $e->getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holt das Log einer Abo-Vorlage.
|
|
||||||
*/
|
|
||||||
function get_recurring_log(int $template_id, int $limit = 20): array {
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->prepare("SELECT rl.*, i.invoice_number, i.total_gross
|
|
||||||
FROM recurring_log rl
|
|
||||||
LEFT JOIN invoices i ON i.id = rl.invoice_id
|
|
||||||
WHERE rl.template_id = :tid
|
|
||||||
ORDER BY rl.generated_at DESC
|
|
||||||
LIMIT :limit");
|
|
||||||
$stmt->bindValue(':tid', $template_id, PDO::PARAM_INT);
|
|
||||||
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
|
||||||
$stmt->execute();
|
|
||||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gibt die Intervall-Bezeichnung auf Deutsch zurück.
|
|
||||||
*/
|
|
||||||
function get_interval_label(string $interval_type): string {
|
|
||||||
return match($interval_type) {
|
|
||||||
'monthly' => 'Monatlich',
|
|
||||||
'quarterly' => 'Quartalsweise',
|
|
||||||
'yearly' => 'Jährlich',
|
|
||||||
default => $interval_type
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Passwort-Hash-Generator für Admin-User
|
|
||||||
if (PHP_SAPI !== 'cli') {
|
|
||||||
echo "Nur CLI.\n";
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
if ($argc < 2) {
|
|
||||||
echo "Verwendung: php tools/hash.php SUPERGEHEIMES_PASSWORT\n";
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
$password = $argv[1];
|
|
||||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
|
||||||
echo $hash . "\n";
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Import: Kategorien aus "Journal 2021.xlsx" in die Datenbank importieren.
|
|
||||||
* Prüft vor jedem Insert auf Duplikate (Name + Type/Side).
|
|
||||||
*
|
|
||||||
* Ausführen: php tools/import_excel_categories.php
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
|
|
||||||
$pdo = get_db();
|
|
||||||
|
|
||||||
echo "Import: Kategorien aus Excel-Tabelle\n";
|
|
||||||
echo str_repeat('=', 50) . "\n";
|
|
||||||
|
|
||||||
$inserted = 0;
|
|
||||||
$skipped = 0;
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 1. journal_revenue_categories - Wareneingang
|
|
||||||
// ============================================================
|
|
||||||
echo "\n--- Wareneingang-Kategorien ---\n";
|
|
||||||
|
|
||||||
$wareneingang = [
|
|
||||||
['name' => 'Handy 0%', 'vat_rate' => 0, 'sort_order' => 1],
|
|
||||||
['name' => 'PVG 7%', 'vat_rate' => 7, 'sort_order' => 2],
|
|
||||||
['name' => 'Zig. 19%', 'vat_rate' => 19, 'sort_order' => 3],
|
|
||||||
['name' => 'Allg. 19%', 'vat_rate' => 19, 'sort_order' => 4],
|
|
||||||
];
|
|
||||||
|
|
||||||
$check_rev = $pdo->prepare('SELECT COUNT(*) FROM journal_revenue_categories WHERE name = :name AND category_type = :type');
|
|
||||||
$insert_rev = $pdo->prepare('INSERT INTO journal_revenue_categories (name, category_type, vat_rate, sort_order, is_active) VALUES (:name, :type, :vat, :sort, TRUE)');
|
|
||||||
|
|
||||||
foreach ($wareneingang as $cat) {
|
|
||||||
$check_rev->execute([':name' => $cat['name'], ':type' => 'wareneingang']);
|
|
||||||
if ($check_rev->fetchColumn() > 0) {
|
|
||||||
echo " SKIP: {$cat['name']} (existiert bereits)\n";
|
|
||||||
$skipped++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$insert_rev->execute([
|
|
||||||
':name' => $cat['name'],
|
|
||||||
':type' => 'wareneingang',
|
|
||||||
':vat' => $cat['vat_rate'],
|
|
||||||
':sort' => $cat['sort_order'],
|
|
||||||
]);
|
|
||||||
echo " OK: {$cat['name']}\n";
|
|
||||||
$inserted++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 2. journal_revenue_categories - Erlöse
|
|
||||||
// ============================================================
|
|
||||||
echo "\n--- Erlös-Kategorien ---\n";
|
|
||||||
|
|
||||||
$erloese = [
|
|
||||||
['name' => 'DP/Handy 0%', 'vat_rate' => 0, 'sort_order' => 1],
|
|
||||||
['name' => 'PVG 7%', 'vat_rate' => 7, 'sort_order' => 2],
|
|
||||||
['name' => 'Zig. 19%', 'vat_rate' => 19, 'sort_order' => 3],
|
|
||||||
['name' => 'Allg. 19%', 'vat_rate' => 19, 'sort_order' => 4],
|
|
||||||
['name' => 'Sonst. 19%', 'vat_rate' => 19, 'sort_order' => 5],
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($erloese as $cat) {
|
|
||||||
$check_rev->execute([':name' => $cat['name'], ':type' => 'erloese']);
|
|
||||||
if ($check_rev->fetchColumn() > 0) {
|
|
||||||
echo " SKIP: {$cat['name']} (existiert bereits)\n";
|
|
||||||
$skipped++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$insert_rev->execute([
|
|
||||||
':name' => $cat['name'],
|
|
||||||
':type' => 'erloese',
|
|
||||||
':vat' => $cat['vat_rate'],
|
|
||||||
':sort' => $cat['sort_order'],
|
|
||||||
]);
|
|
||||||
echo " OK: {$cat['name']}\n";
|
|
||||||
$inserted++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 3. journal_deduction_categories
|
|
||||||
// ============================================================
|
|
||||||
echo "\n--- Abzugs-Kategorien ---\n";
|
|
||||||
|
|
||||||
$deductions = [
|
|
||||||
['name' => 'Skonto', 'sort_order' => 1],
|
|
||||||
['name' => 'Lotto', 'sort_order' => 2],
|
|
||||||
];
|
|
||||||
|
|
||||||
$check_ded = $pdo->prepare('SELECT COUNT(*) FROM journal_deduction_categories WHERE name = :name');
|
|
||||||
$insert_ded = $pdo->prepare('INSERT INTO journal_deduction_categories (name, sort_order, is_active) VALUES (:name, :sort, TRUE)');
|
|
||||||
|
|
||||||
foreach ($deductions as $cat) {
|
|
||||||
$check_ded->execute([':name' => $cat['name']]);
|
|
||||||
if ($check_ded->fetchColumn() > 0) {
|
|
||||||
echo " SKIP: {$cat['name']} (existiert bereits)\n";
|
|
||||||
$skipped++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$insert_ded->execute([
|
|
||||||
':name' => $cat['name'],
|
|
||||||
':sort' => $cat['sort_order'],
|
|
||||||
]);
|
|
||||||
echo " OK: {$cat['name']}\n";
|
|
||||||
$inserted++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 4. journal_expense_categories (alle side=soll)
|
|
||||||
// ============================================================
|
|
||||||
echo "\n--- Aufwands-Kategorien ---\n";
|
|
||||||
|
|
||||||
$expenses = [
|
|
||||||
['name' => 'Bankunkosten', 'sort_order' => 1],
|
|
||||||
['name' => 'KFZ-kosten', 'sort_order' => 2],
|
|
||||||
['name' => 'Personalkosten', 'sort_order' => 3],
|
|
||||||
['name' => 'Raumkosten', 'sort_order' => 4],
|
|
||||||
['name' => 'Tel./Fax', 'sort_order' => 5],
|
|
||||||
['name' => 'Allg./Werbekosten', 'sort_order' => 6],
|
|
||||||
['name' => 'Sonst. Deko', 'sort_order' => 7],
|
|
||||||
['name' => 'Bezugsnebenko.', 'sort_order' => 8],
|
|
||||||
];
|
|
||||||
|
|
||||||
$check_exp = $pdo->prepare('SELECT COUNT(*) FROM journal_expense_categories WHERE name = :name AND side = :side');
|
|
||||||
$insert_exp = $pdo->prepare('INSERT INTO journal_expense_categories (name, side, sort_order, is_active) VALUES (:name, :side, :sort, TRUE)');
|
|
||||||
|
|
||||||
foreach ($expenses as $cat) {
|
|
||||||
$check_exp->execute([':name' => $cat['name'], ':side' => 'soll']);
|
|
||||||
if ($check_exp->fetchColumn() > 0) {
|
|
||||||
echo " SKIP: {$cat['name']} (existiert bereits)\n";
|
|
||||||
$skipped++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$insert_exp->execute([
|
|
||||||
':name' => $cat['name'],
|
|
||||||
':side' => 'soll',
|
|
||||||
':sort' => $cat['sort_order'],
|
|
||||||
]);
|
|
||||||
echo " OK: {$cat['name']}\n";
|
|
||||||
$inserted++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Ergebnis
|
|
||||||
// ============================================================
|
|
||||||
echo "\n" . str_repeat('=', 50) . "\n";
|
|
||||||
echo "Fertig! Eingefügt: $inserted, Übersprungen: $skipped\n";
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Migration: Erstellt journal_deduction_categories Tabelle für customizable Abzüge
|
|
||||||
* Ausführen: php tools/migrate_deductions.php
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
|
|
||||||
$pdo = get_db();
|
|
||||||
|
|
||||||
echo "Migration: journal_deduction_categories\n";
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Prüfen ob Tabelle existiert
|
|
||||||
$stmt = $pdo->query("SELECT to_regclass('public.journal_deduction_categories')");
|
|
||||||
$exists = $stmt->fetchColumn();
|
|
||||||
|
|
||||||
if ($exists) {
|
|
||||||
echo "Tabelle journal_deduction_categories existiert bereits.\n";
|
|
||||||
} else {
|
|
||||||
// Tabelle erstellen
|
|
||||||
$pdo->exec("CREATE TABLE journal_deduction_categories (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name VARCHAR(100) NOT NULL,
|
|
||||||
sort_order INTEGER DEFAULT 0,
|
|
||||||
is_active BOOLEAN DEFAULT TRUE,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)");
|
|
||||||
echo "Tabelle journal_deduction_categories erstellt.\n";
|
|
||||||
|
|
||||||
// Standard-Abzüge einfügen
|
|
||||||
$pdo->exec("INSERT INTO journal_deduction_categories (name, sort_order, is_active) VALUES
|
|
||||||
('Skonto', 1, TRUE),
|
|
||||||
('Lotto', 2, TRUE)
|
|
||||||
");
|
|
||||||
echo "Standard-Abzüge (Skonto, Lotto) eingefügt.\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Migration erfolgreich!\n";
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
echo "Fehler: " . $e->getMessage() . "\n";
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
-- PIRP Vollständige Migration (pirp -> pirp-dev)
|
|
||||||
-- Erstellt am: 2026-02-07
|
|
||||||
--
|
|
||||||
-- Führt alle neuen Migrationen in der richtigen Reihenfolge aus:
|
|
||||||
-- 1. invoice_id für journal_entries (Rechnungsverknüpfung)
|
|
||||||
-- 2. Zahlungsdaten und MwSt-Felder
|
|
||||||
-- 3. Unique Constraints
|
|
||||||
|
|
||||||
BEGIN;
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- 1. Journal-Rechnungen-Verknüpfung (migrate_journal_invoice_link.sql)
|
|
||||||
-- ============================================================
|
|
||||||
ALTER TABLE journal_entries ADD COLUMN IF NOT EXISTS invoice_id INTEGER REFERENCES invoices(id) ON DELETE SET NULL;
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_journal_entries_invoice ON journal_entries(invoice_id);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- 2. Journal-Auto-Buchung (migrate_journal_auto.sql)
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
-- 2a. Rechnungen: Zahlungsdatum hinzufügen
|
|
||||||
ALTER TABLE invoices ADD COLUMN IF NOT EXISTS payment_date DATE;
|
|
||||||
|
|
||||||
-- Bestehende bezahlte Rechnungen: invoice_date als Fallback
|
|
||||||
UPDATE invoices SET payment_date = invoice_date WHERE paid = TRUE AND payment_date IS NULL;
|
|
||||||
|
|
||||||
-- 2b. Ausgaben: MwSt-Felder und Kategorien-Verknüpfung
|
|
||||||
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS vat_rate NUMERIC(5,2) DEFAULT 0;
|
|
||||||
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS total_net NUMERIC(12,2);
|
|
||||||
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS total_vat NUMERIC(12,2) DEFAULT 0;
|
|
||||||
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS expense_category_id INTEGER REFERENCES journal_expense_categories(id) ON DELETE SET NULL;
|
|
||||||
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS payment_date DATE;
|
|
||||||
|
|
||||||
-- Bestehende Ausgaben: amount als Netto (kein MwSt-Split bekannt), Zahlungsdatum = Ausgabedatum
|
|
||||||
UPDATE expenses SET total_net = amount, total_vat = 0 WHERE total_net IS NULL;
|
|
||||||
UPDATE expenses SET payment_date = expense_date WHERE paid = TRUE AND payment_date IS NULL;
|
|
||||||
|
|
||||||
-- 2c. Journal-Einträge: Ausgaben-Verknüpfung und Quell-Typ
|
|
||||||
ALTER TABLE journal_entries ADD COLUMN IF NOT EXISTS expense_id INTEGER REFERENCES expenses(id) ON DELETE SET NULL;
|
|
||||||
ALTER TABLE journal_entries ADD COLUMN IF NOT EXISTS source_type VARCHAR(20) DEFAULT 'manual';
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_journal_entries_expense ON journal_entries(expense_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_journal_entries_source ON journal_entries(source_type);
|
|
||||||
|
|
||||||
-- Bestehende Rechnungs-Buchungen als source_type markieren
|
|
||||||
UPDATE journal_entries SET source_type = 'invoice_payment' WHERE invoice_id IS NOT NULL AND source_type = 'manual';
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- 3. Unique Constraints (migrate_unique_constraints.sql)
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
-- Zuerst evtl. vorhandene Duplikate bereinigen (behalte nur den neuesten)
|
|
||||||
DELETE FROM journal_entries a
|
|
||||||
USING journal_entries b
|
|
||||||
WHERE a.invoice_id IS NOT NULL
|
|
||||||
AND a.invoice_id = b.invoice_id
|
|
||||||
AND a.id < b.id;
|
|
||||||
|
|
||||||
DELETE FROM journal_entries a
|
|
||||||
USING journal_entries b
|
|
||||||
WHERE a.expense_id IS NOT NULL
|
|
||||||
AND a.expense_id = b.expense_id
|
|
||||||
AND a.id < b.id;
|
|
||||||
|
|
||||||
-- Unique-Indexes erstellen (nur wenn nicht vorhanden)
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_journal_invoice ON journal_entries(invoice_id) WHERE invoice_id IS NOT NULL;
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_journal_expense ON journal_entries(expense_id) WHERE expense_id IS NOT NULL;
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
|
|
||||||
-- Erfolgsmeldung
|
|
||||||
SELECT 'Migration erfolgreich abgeschlossen' AS status;
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
-- Journal-Modul: Buchführungsjournal für PIRP
|
|
||||||
-- Migration: Alle Journal-Tabellen erstellen
|
|
||||||
|
|
||||||
-- Jahre
|
|
||||||
CREATE TABLE IF NOT EXISTS journal_years (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
year INTEGER NOT NULL UNIQUE,
|
|
||||||
is_closed BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
notes TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Lieferanten
|
|
||||||
CREATE TABLE IF NOT EXISTS journal_suppliers (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Erlös-/Wareneingang-Kategorien
|
|
||||||
CREATE TABLE IF NOT EXISTS journal_revenue_categories (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
category_type VARCHAR(20) NOT NULL CHECK (category_type IN ('wareneingang', 'erloese')),
|
|
||||||
vat_rate NUMERIC(5,2) NOT NULL DEFAULT 19.00,
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Aufwandskategorien
|
|
||||||
CREATE TABLE IF NOT EXISTS journal_expense_categories (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
side VARCHAR(10) NOT NULL DEFAULT 'soll' CHECK (side IN ('soll', 'soll_haben')),
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Journal-Einträge (Buchungen)
|
|
||||||
CREATE TABLE IF NOT EXISTS journal_entries (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
year_id INTEGER NOT NULL REFERENCES journal_years(id) ON DELETE RESTRICT,
|
|
||||||
entry_date DATE NOT NULL,
|
|
||||||
month INTEGER NOT NULL CHECK (month BETWEEN 1 AND 12),
|
|
||||||
description TEXT NOT NULL,
|
|
||||||
attachment_note TEXT,
|
|
||||||
amount NUMERIC(12,2) NOT NULL DEFAULT 0,
|
|
||||||
supplier_id INTEGER REFERENCES journal_suppliers(id) ON DELETE SET NULL,
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Kontenverteilung pro Buchung
|
|
||||||
CREATE TABLE IF NOT EXISTS journal_entry_accounts (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
entry_id INTEGER NOT NULL REFERENCES journal_entries(id) ON DELETE CASCADE,
|
|
||||||
account_type VARCHAR(20) NOT NULL,
|
|
||||||
side VARCHAR(5) NOT NULL CHECK (side IN ('soll', 'haben')),
|
|
||||||
amount NUMERIC(12,2) NOT NULL DEFAULT 0,
|
|
||||||
revenue_category_id INTEGER REFERENCES journal_revenue_categories(id) ON DELETE SET NULL,
|
|
||||||
expense_category_id INTEGER REFERENCES journal_expense_categories(id) ON DELETE SET NULL,
|
|
||||||
note TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Monatliche Zusammenfassung (manuelle Korrekturen)
|
|
||||||
CREATE TABLE IF NOT EXISTS journal_monthly_summary (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
year_id INTEGER NOT NULL REFERENCES journal_years(id) ON DELETE CASCADE,
|
|
||||||
month INTEGER NOT NULL CHECK (month BETWEEN 1 AND 12),
|
|
||||||
manual_corrections JSONB DEFAULT '{}',
|
|
||||||
notes TEXT,
|
|
||||||
UNIQUE(year_id, month)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Umsatz-Zusammenfassungsposten (z.B. "Reinigung", "RMV", "Handy")
|
|
||||||
CREATE TABLE IF NOT EXISTS journal_summary_items (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Monatliche Werte für Zusammenfassungsposten
|
|
||||||
CREATE TABLE IF NOT EXISTS journal_monthly_summary_values (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
year_id INTEGER NOT NULL REFERENCES journal_years(id) ON DELETE CASCADE,
|
|
||||||
month INTEGER NOT NULL CHECK (month BETWEEN 1 AND 12),
|
|
||||||
summary_item_id INTEGER NOT NULL REFERENCES journal_summary_items(id) ON DELETE CASCADE,
|
|
||||||
amount NUMERIC(12,2) NOT NULL DEFAULT 0,
|
|
||||||
UNIQUE(year_id, month, summary_item_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Indizes für Performance
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_journal_entries_year_month ON journal_entries(year_id, month);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_journal_entries_date ON journal_entries(entry_date);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_journal_entry_accounts_entry ON journal_entry_accounts(entry_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_journal_entry_accounts_type ON journal_entry_accounts(account_type);
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
-- Journal-Auto-Buchung: Datenbank-Migration
|
|
||||||
-- Erweitert invoices, expenses und journal_entries für automatische Journaleinträge
|
|
||||||
-- Zufluss-/Abflussprinzip (§ 11 EStG)
|
|
||||||
|
|
||||||
-- 1. Rechnungen: Zahlungsdatum hinzufügen
|
|
||||||
ALTER TABLE invoices ADD COLUMN IF NOT EXISTS payment_date DATE;
|
|
||||||
|
|
||||||
-- Bestehende bezahlte Rechnungen: invoice_date als Fallback
|
|
||||||
UPDATE invoices SET payment_date = invoice_date WHERE paid = TRUE AND payment_date IS NULL;
|
|
||||||
|
|
||||||
-- 2. Ausgaben: MwSt-Felder und Kategorien-Verknüpfung
|
|
||||||
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS vat_rate NUMERIC(5,2) DEFAULT 0;
|
|
||||||
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS total_net NUMERIC(12,2);
|
|
||||||
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS total_vat NUMERIC(12,2) DEFAULT 0;
|
|
||||||
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS expense_category_id INTEGER REFERENCES journal_expense_categories(id) ON DELETE SET NULL;
|
|
||||||
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS payment_date DATE;
|
|
||||||
|
|
||||||
-- Bestehende Ausgaben: amount als Netto (kein MwSt-Split bekannt), Zahlungsdatum = Ausgabedatum
|
|
||||||
UPDATE expenses SET total_net = amount, total_vat = 0 WHERE total_net IS NULL;
|
|
||||||
UPDATE expenses SET payment_date = expense_date WHERE paid = TRUE AND payment_date IS NULL;
|
|
||||||
|
|
||||||
-- 3. Journal-Einträge: Ausgaben-Verknüpfung und Quell-Typ
|
|
||||||
ALTER TABLE journal_entries ADD COLUMN IF NOT EXISTS expense_id INTEGER REFERENCES expenses(id) ON DELETE SET NULL;
|
|
||||||
ALTER TABLE journal_entries ADD COLUMN IF NOT EXISTS source_type VARCHAR(20) DEFAULT 'manual';
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_journal_entries_expense ON journal_entries(expense_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_journal_entries_source ON journal_entries(source_type);
|
|
||||||
|
|
||||||
-- Bestehende Rechnungs-Buchungen als source_type markieren
|
|
||||||
UPDATE journal_entries SET source_type = 'invoice_payment' WHERE invoice_id IS NOT NULL AND source_type = 'manual';
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Migrationstool: Erstellt nachträglich Journalbuchungen für alle bezahlten
|
|
||||||
* Rechnungen und Ausgaben, die noch keinen Journaleintrag haben.
|
|
||||||
*
|
|
||||||
* Setzt außerdem payment_date für bestehende bezahlte Belege, falls noch nicht gesetzt.
|
|
||||||
*
|
|
||||||
* Ausführung: php tools/migrate_journal_entries.php
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_once __DIR__ . '/../src/invoice_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/journal_functions.php';
|
|
||||||
require_once __DIR__ . '/../src/expense_functions.php';
|
|
||||||
|
|
||||||
echo "=== PIRP Journal-Migration ===\n\n";
|
|
||||||
|
|
||||||
$pdo = get_db();
|
|
||||||
|
|
||||||
// 1. payment_date für bezahlte Rechnungen setzen (Fallback: invoice_date)
|
|
||||||
echo "1. Payment-Dates für Rechnungen setzen...\n";
|
|
||||||
$stmt = $pdo->query("UPDATE invoices SET payment_date = invoice_date WHERE paid = TRUE AND payment_date IS NULL");
|
|
||||||
$count = $stmt->rowCount();
|
|
||||||
echo " $count Rechnungen aktualisiert.\n\n";
|
|
||||||
|
|
||||||
// 2. payment_date für bezahlte Ausgaben setzen (Fallback: expense_date)
|
|
||||||
echo "2. Payment-Dates für Ausgaben setzen...\n";
|
|
||||||
$stmt = $pdo->query("UPDATE expenses SET payment_date = expense_date WHERE paid = TRUE AND payment_date IS NULL");
|
|
||||||
$count = $stmt->rowCount();
|
|
||||||
echo " $count Ausgaben aktualisiert.\n\n";
|
|
||||||
|
|
||||||
// 3. total_net für Ausgaben setzen (Fallback: amount, keine MwSt)
|
|
||||||
echo "3. Netto-Beträge für Ausgaben setzen...\n";
|
|
||||||
$stmt = $pdo->query("UPDATE expenses SET total_net = amount, total_vat = 0 WHERE total_net IS NULL");
|
|
||||||
$count = $stmt->rowCount();
|
|
||||||
echo " $count Ausgaben aktualisiert.\n\n";
|
|
||||||
|
|
||||||
// 4. Bezahlte Rechnungen ohne Journaleintrag
|
|
||||||
echo "4. Bezahlte Rechnungen ohne Journalbuchung...\n";
|
|
||||||
$stmt = $pdo->query("SELECT i.id, i.invoice_number, i.total_gross, i.payment_date, c.name AS customer_name
|
|
||||||
FROM invoices i
|
|
||||||
JOIN customers c ON c.id = i.customer_id
|
|
||||||
LEFT JOIN journal_entries je ON je.invoice_id = i.id
|
|
||||||
WHERE i.paid = TRUE AND je.id IS NULL
|
|
||||||
ORDER BY i.invoice_date ASC");
|
|
||||||
$unbooked_invoices = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
$inv_ok = 0;
|
|
||||||
$inv_err = 0;
|
|
||||||
foreach ($unbooked_invoices as $inv) {
|
|
||||||
try {
|
|
||||||
$entry_id = create_journal_entry_from_invoice((int)$inv['id']);
|
|
||||||
echo " OK: {$inv['invoice_number']} ({$inv['customer_name']}) -> Journal #{$entry_id}\n";
|
|
||||||
$inv_ok++;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo " FEHLER: {$inv['invoice_number']}: {$e->getMessage()}\n";
|
|
||||||
$inv_err++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
echo " Ergebnis: $inv_ok erstellt, $inv_err Fehler.\n\n";
|
|
||||||
|
|
||||||
// 5. Bezahlte Ausgaben ohne Journaleintrag
|
|
||||||
echo "5. Bezahlte Ausgaben ohne Journalbuchung...\n";
|
|
||||||
$stmt = $pdo->query("SELECT e.id, e.description, e.amount, e.payment_date
|
|
||||||
FROM expenses e
|
|
||||||
LEFT JOIN journal_entries je ON je.expense_id = e.id
|
|
||||||
WHERE e.paid = TRUE AND je.id IS NULL
|
|
||||||
ORDER BY e.expense_date ASC");
|
|
||||||
$unbooked_expenses = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
$exp_ok = 0;
|
|
||||||
$exp_err = 0;
|
|
||||||
foreach ($unbooked_expenses as $exp) {
|
|
||||||
try {
|
|
||||||
$entry_id = create_journal_entry_from_expense((int)$exp['id']);
|
|
||||||
echo " OK: \"{$exp['description']}\" ({$exp['amount']} EUR) -> Journal #{$entry_id}\n";
|
|
||||||
$exp_ok++;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo " FEHLER: \"{$exp['description']}\": {$e->getMessage()}\n";
|
|
||||||
$exp_err++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
echo " Ergebnis: $exp_ok erstellt, $exp_err Fehler.\n\n";
|
|
||||||
|
|
||||||
// Zusammenfassung
|
|
||||||
echo "=== Zusammenfassung ===\n";
|
|
||||||
echo "Rechnungen: $inv_ok gebucht" . ($inv_err > 0 ? ", $inv_err Fehler" : '') . "\n";
|
|
||||||
echo "Ausgaben: $exp_ok gebucht" . ($exp_err > 0 ? ", $exp_err Fehler" : '') . "\n";
|
|
||||||
|
|
||||||
$total = $inv_ok + $exp_ok;
|
|
||||||
if ($total === 0) {
|
|
||||||
echo "\nKeine fehlenden Buchungen gefunden. Alles aktuell.\n";
|
|
||||||
} else {
|
|
||||||
echo "\n$total Buchungen insgesamt erstellt.\n";
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Migration: Fügt invoice_id Spalte zu journal_entries hinzu
|
|
||||||
* Ausführen: php tools/migrate_journal_invoice_link.php
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
|
|
||||||
$pdo = get_db();
|
|
||||||
|
|
||||||
echo "Migration: journal_entries.invoice_id\n";
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Prüfen ob Spalte existiert
|
|
||||||
$stmt = $pdo->query("SELECT column_name FROM information_schema.columns
|
|
||||||
WHERE table_name = 'journal_entries' AND column_name = 'invoice_id'");
|
|
||||||
$exists = $stmt->fetch();
|
|
||||||
|
|
||||||
if ($exists) {
|
|
||||||
echo "Spalte invoice_id existiert bereits.\n";
|
|
||||||
} else {
|
|
||||||
// Spalte hinzufügen
|
|
||||||
$pdo->exec("ALTER TABLE journal_entries ADD COLUMN invoice_id INTEGER REFERENCES invoices(id) ON DELETE SET NULL");
|
|
||||||
echo "Spalte invoice_id hinzugefügt.\n";
|
|
||||||
|
|
||||||
// Index erstellen
|
|
||||||
$pdo->exec("CREATE INDEX IF NOT EXISTS idx_journal_entries_invoice ON journal_entries(invoice_id)");
|
|
||||||
echo "Index erstellt.\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Migration erfolgreich!\n";
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
echo "Fehler: " . $e->getMessage() . "\n";
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
-- Journal-Rechnungen-Verknüpfung
|
|
||||||
-- Fügt invoice_id zu journal_entries hinzu
|
|
||||||
|
|
||||||
ALTER TABLE journal_entries ADD COLUMN IF NOT EXISTS invoice_id INTEGER REFERENCES invoices(id) ON DELETE SET NULL;
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_journal_entries_invoice ON journal_entries(invoice_id);
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
-- GoBD-konforme PDF-Speicherung: Erweiterung der invoices-Tabelle
|
|
||||||
-- Migration für unveränderliche PDF-Archivierung
|
|
||||||
-- Ausführen mit: psql -U pirp_user -d pirp -f tools/migrate_pdf.sql
|
|
||||||
|
|
||||||
-- Pfad zur archivierten PDF-Datei (relativ zu public/uploads/invoices/)
|
|
||||||
ALTER TABLE invoices ADD COLUMN IF NOT EXISTS pdf_path TEXT;
|
|
||||||
|
|
||||||
-- SHA-256 Hash des PDF-Inhalts zur Integritätsprüfung
|
|
||||||
ALTER TABLE invoices ADD COLUMN IF NOT EXISTS pdf_hash VARCHAR(64);
|
|
||||||
|
|
||||||
-- Zeitstempel der PDF-Generierung
|
|
||||||
ALTER TABLE invoices ADD COLUMN IF NOT EXISTS pdf_generated_at TIMESTAMPTZ;
|
|
||||||
|
|
||||||
-- Index für schnelle Abfragen nach fehlenden PDFs
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_invoices_pdf_path ON invoices(pdf_path) WHERE pdf_path IS NULL;
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Migrationsscript: Generiert PDFs für alle bestehenden Rechnungen
|
|
||||||
*
|
|
||||||
* ACHTUNG: Einmalig ausführen!
|
|
||||||
* Bei bestehenden Rechnungen werden die AKTUELLEN Firmen-/Kundendaten verwendet.
|
|
||||||
*
|
|
||||||
* Ausführung: php tools/migrate_pdfs.php
|
|
||||||
*/
|
|
||||||
if (PHP_SAPI !== 'cli') {
|
|
||||||
echo "Nur CLI.\n";
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
require_once __DIR__ . '/../src/pdf_functions.php';
|
|
||||||
|
|
||||||
echo "PDF-Migration gestartet...\n";
|
|
||||||
|
|
||||||
// Verzeichnis erstellen falls nicht vorhanden
|
|
||||||
$uploadDir = __DIR__ . '/../public/uploads/invoices';
|
|
||||||
if (!is_dir($uploadDir)) {
|
|
||||||
mkdir($uploadDir, 0775, true);
|
|
||||||
echo "Verzeichnis erstellt: $uploadDir\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo = get_db();
|
|
||||||
$stmt = $pdo->query("SELECT id, invoice_number FROM invoices WHERE pdf_path IS NULL ORDER BY id");
|
|
||||||
$invoices = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
echo "Gefunden: " . count($invoices) . " Rechnungen ohne archivierte PDF\n";
|
|
||||||
|
|
||||||
$success = 0;
|
|
||||||
$failed = 0;
|
|
||||||
|
|
||||||
foreach ($invoices as $inv) {
|
|
||||||
echo "Verarbeite {$inv['invoice_number']}... ";
|
|
||||||
|
|
||||||
$result = archive_invoice_pdf($inv['id']);
|
|
||||||
|
|
||||||
if ($result) {
|
|
||||||
echo "OK -> $result\n";
|
|
||||||
$success++;
|
|
||||||
} else {
|
|
||||||
echo "FEHLER\n";
|
|
||||||
$failed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\nMigration abgeschlossen.\n";
|
|
||||||
echo "Erfolgreich: $success\n";
|
|
||||||
echo "Fehlgeschlagen: $failed\n";
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
-- Wiederkehrende Rechnungen (Abo-Rechnungen)
|
|
||||||
-- Migration ausführen mit: psql -U pirp_user -d pirp -f tools/migrate_recurring.sql
|
|
||||||
|
|
||||||
-- Abo-Vorlagen
|
|
||||||
CREATE TABLE IF NOT EXISTS recurring_templates (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
template_name VARCHAR(100) NOT NULL,
|
|
||||||
customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE RESTRICT,
|
|
||||||
interval_type VARCHAR(20) NOT NULL CHECK (interval_type IN ('monthly', 'quarterly', 'yearly')),
|
|
||||||
start_date DATE NOT NULL,
|
|
||||||
end_date DATE,
|
|
||||||
next_due_date DATE NOT NULL,
|
|
||||||
vat_mode VARCHAR(10) NOT NULL DEFAULT 'klein',
|
|
||||||
vat_rate NUMERIC(5,2) NOT NULL DEFAULT 19.00,
|
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
||||||
notes_internal TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Positionen der Abo-Vorlage
|
|
||||||
CREATE TABLE IF NOT EXISTS recurring_template_items (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
template_id INTEGER NOT NULL REFERENCES recurring_templates(id) ON DELETE CASCADE,
|
|
||||||
position_no INTEGER NOT NULL,
|
|
||||||
description TEXT NOT NULL,
|
|
||||||
quantity NUMERIC(12,2) NOT NULL DEFAULT 1,
|
|
||||||
unit_price NUMERIC(12,2) NOT NULL DEFAULT 0
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Log der generierten Rechnungen
|
|
||||||
CREATE TABLE IF NOT EXISTS recurring_log (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
template_id INTEGER NOT NULL REFERENCES recurring_templates(id) ON DELETE CASCADE,
|
|
||||||
invoice_id INTEGER REFERENCES invoices(id) ON DELETE SET NULL,
|
|
||||||
generated_at TIMESTAMPTZ DEFAULT now(),
|
|
||||||
due_date DATE NOT NULL,
|
|
||||||
status VARCHAR(20) NOT NULL DEFAULT 'generated'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Indices
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_recurring_next_due ON recurring_templates(next_due_date) WHERE is_active = TRUE;
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_recurring_log_template ON recurring_log(template_id);
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
-- Migration: Storno-Rechnungen + Mahnungen
|
|
||||||
-- Anwendung: psql -U pirp_user -d pirp -f tools/migrate_storno_mahnung.sql
|
|
||||||
|
|
||||||
-- Storno: zwei neue Spalten in invoices
|
|
||||||
ALTER TABLE invoices
|
|
||||||
ADD COLUMN IF NOT EXISTS storno_of INTEGER REFERENCES invoices(id) ON DELETE SET NULL,
|
|
||||||
ADD COLUMN IF NOT EXISTS is_storno BOOLEAN NOT NULL DEFAULT FALSE;
|
|
||||||
|
|
||||||
-- Mahnungen: neue Tabelle
|
|
||||||
CREATE TABLE IF NOT EXISTS mahnungen (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
invoice_id INTEGER NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
|
|
||||||
mahnung_date DATE NOT NULL,
|
|
||||||
level INTEGER NOT NULL DEFAULT 1 CHECK (level IN (1,2,3)),
|
|
||||||
fee_amount NUMERIC(12,2) NOT NULL DEFAULT 0,
|
|
||||||
pdf_path TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_mahnungen_invoice_id ON mahnungen(invoice_id);
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
-- Migration: Unique-Constraints für Journal-Einträge
|
|
||||||
-- Verhindert Doppelbuchungen (max. 1 Journal-Eintrag pro Rechnung/Ausgabe)
|
|
||||||
|
|
||||||
-- Zuerst evtl. vorhandene Duplikate bereinigen (behalte nur den neuesten)
|
|
||||||
DELETE FROM journal_entries a
|
|
||||||
USING journal_entries b
|
|
||||||
WHERE a.invoice_id IS NOT NULL
|
|
||||||
AND a.invoice_id = b.invoice_id
|
|
||||||
AND a.id < b.id;
|
|
||||||
|
|
||||||
DELETE FROM journal_entries a
|
|
||||||
USING journal_entries b
|
|
||||||
WHERE a.expense_id IS NOT NULL
|
|
||||||
AND a.expense_id = b.expense_id
|
|
||||||
AND a.id < b.id;
|
|
||||||
|
|
||||||
-- Unique-Indexes erstellen
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_journal_invoice ON journal_entries(invoice_id) WHERE invoice_id IS NOT NULL;
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_journal_expense ON journal_entries(expense_id) WHERE expense_id IS NOT NULL;
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Einmalige Migration: Spalten hinzufügen und PDFs generieren
|
|
||||||
*/
|
|
||||||
if (PHP_SAPI !== 'cli') {
|
|
||||||
die("Nur CLI.\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../src/config.php';
|
|
||||||
require_once __DIR__ . '/../src/db.php';
|
|
||||||
|
|
||||||
$pdo = get_db();
|
|
||||||
|
|
||||||
echo "1. Prüfe/erstelle Spalten...\n";
|
|
||||||
|
|
||||||
// Spalten hinzufügen falls nicht vorhanden
|
|
||||||
$migrations = [
|
|
||||||
"ALTER TABLE invoices ADD COLUMN IF NOT EXISTS pdf_path TEXT",
|
|
||||||
"ALTER TABLE invoices ADD COLUMN IF NOT EXISTS pdf_hash VARCHAR(64)",
|
|
||||||
"ALTER TABLE invoices ADD COLUMN IF NOT EXISTS pdf_generated_at TIMESTAMPTZ",
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($migrations as $sql) {
|
|
||||||
try {
|
|
||||||
$pdo->exec($sql);
|
|
||||||
echo " OK: $sql\n";
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
echo " FEHLER: " . $e->getMessage() . "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Index erstellen
|
|
||||||
try {
|
|
||||||
$pdo->exec("CREATE INDEX IF NOT EXISTS idx_invoices_pdf_path ON invoices(pdf_path) WHERE pdf_path IS NULL");
|
|
||||||
echo " OK: Index erstellt\n";
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
// Index existiert möglicherweise schon
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\n2. Generiere PDFs für alte Rechnungen...\n";
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../src/pdf_functions.php';
|
|
||||||
|
|
||||||
// Verzeichnis erstellen
|
|
||||||
$uploadDir = __DIR__ . '/../public/uploads/invoices';
|
|
||||||
if (!is_dir($uploadDir)) {
|
|
||||||
mkdir($uploadDir, 0775, true);
|
|
||||||
echo " Verzeichnis erstellt: $uploadDir\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt = $pdo->query("SELECT id, invoice_number FROM invoices WHERE pdf_path IS NULL ORDER BY id");
|
|
||||||
$invoices = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
echo " Gefunden: " . count($invoices) . " Rechnungen ohne PDF\n\n";
|
|
||||||
|
|
||||||
$success = 0;
|
|
||||||
$failed = 0;
|
|
||||||
|
|
||||||
foreach ($invoices as $inv) {
|
|
||||||
echo " Verarbeite {$inv['invoice_number']}... ";
|
|
||||||
|
|
||||||
$result = archive_invoice_pdf($inv['id']);
|
|
||||||
|
|
||||||
if ($result) {
|
|
||||||
echo "OK -> $result\n";
|
|
||||||
$success++;
|
|
||||||
} else {
|
|
||||||
echo "FEHLER\n";
|
|
||||||
$failed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\n=== Migration abgeschlossen ===\n";
|
|
||||||
echo "Erfolgreich: $success\n";
|
|
||||||
echo "Fehlgeschlagen: $failed\n";
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
-- Dev-Seed: Default-Login admin:admin
|
|
||||||
INSERT INTO users (username, password_hash)
|
|
||||||
VALUES ('admin', '$2b$10$PDnwnHN/2V3HrYqnfZtZUOtSHkIynW9olAp9W9RB4FZsh7KbKW5jq')
|
|
||||||
ON CONFLICT (username) DO NOTHING;
|
|
||||||
25
pirp/vendor/autoload.php
vendored
25
pirp/vendor/autoload.php
vendored
@@ -1,25 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
// autoload.php @generated by Composer
|
|
||||||
|
|
||||||
if (PHP_VERSION_ID < 50600) {
|
|
||||||
if (!headers_sent()) {
|
|
||||||
header('HTTP/1.1 500 Internal Server Error');
|
|
||||||
}
|
|
||||||
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
|
||||||
if (!ini_get('display_errors')) {
|
|
||||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
|
||||||
fwrite(STDERR, $err);
|
|
||||||
} elseif (!headers_sent()) {
|
|
||||||
echo $err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
trigger_error(
|
|
||||||
$err,
|
|
||||||
E_USER_ERROR
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once __DIR__ . '/composer/autoload_real.php';
|
|
||||||
|
|
||||||
return ComposerAutoloaderInit76bd9531733da7ca24f4b785b8fe430d::getLoader();
|
|
||||||
579
pirp/vendor/composer/ClassLoader.php
vendored
579
pirp/vendor/composer/ClassLoader.php
vendored
@@ -1,579 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of Composer.
|
|
||||||
*
|
|
||||||
* (c) Nils Adermann <naderman@naderman.de>
|
|
||||||
* Jordi Boggiano <j.boggiano@seld.be>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Composer\Autoload;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
|
|
||||||
*
|
|
||||||
* $loader = new \Composer\Autoload\ClassLoader();
|
|
||||||
*
|
|
||||||
* // register classes with namespaces
|
|
||||||
* $loader->add('Symfony\Component', __DIR__.'/component');
|
|
||||||
* $loader->add('Symfony', __DIR__.'/framework');
|
|
||||||
*
|
|
||||||
* // activate the autoloader
|
|
||||||
* $loader->register();
|
|
||||||
*
|
|
||||||
* // to enable searching the include path (eg. for PEAR packages)
|
|
||||||
* $loader->setUseIncludePath(true);
|
|
||||||
*
|
|
||||||
* In this example, if you try to use a class in the Symfony\Component
|
|
||||||
* namespace or one of its children (Symfony\Component\Console for instance),
|
|
||||||
* the autoloader will first look for the class under the component/
|
|
||||||
* directory, and it will then fallback to the framework/ directory if not
|
|
||||||
* found before giving up.
|
|
||||||
*
|
|
||||||
* This class is loosely based on the Symfony UniversalClassLoader.
|
|
||||||
*
|
|
||||||
* @author Fabien Potencier <fabien@symfony.com>
|
|
||||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
|
||||||
* @see https://www.php-fig.org/psr/psr-0/
|
|
||||||
* @see https://www.php-fig.org/psr/psr-4/
|
|
||||||
*/
|
|
||||||
class ClassLoader
|
|
||||||
{
|
|
||||||
/** @var \Closure(string):void */
|
|
||||||
private static $includeFile;
|
|
||||||
|
|
||||||
/** @var string|null */
|
|
||||||
private $vendorDir;
|
|
||||||
|
|
||||||
// PSR-4
|
|
||||||
/**
|
|
||||||
* @var array<string, array<string, int>>
|
|
||||||
*/
|
|
||||||
private $prefixLengthsPsr4 = array();
|
|
||||||
/**
|
|
||||||
* @var array<string, list<string>>
|
|
||||||
*/
|
|
||||||
private $prefixDirsPsr4 = array();
|
|
||||||
/**
|
|
||||||
* @var list<string>
|
|
||||||
*/
|
|
||||||
private $fallbackDirsPsr4 = array();
|
|
||||||
|
|
||||||
// PSR-0
|
|
||||||
/**
|
|
||||||
* List of PSR-0 prefixes
|
|
||||||
*
|
|
||||||
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
|
|
||||||
*
|
|
||||||
* @var array<string, array<string, list<string>>>
|
|
||||||
*/
|
|
||||||
private $prefixesPsr0 = array();
|
|
||||||
/**
|
|
||||||
* @var list<string>
|
|
||||||
*/
|
|
||||||
private $fallbackDirsPsr0 = array();
|
|
||||||
|
|
||||||
/** @var bool */
|
|
||||||
private $useIncludePath = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array<string, string>
|
|
||||||
*/
|
|
||||||
private $classMap = array();
|
|
||||||
|
|
||||||
/** @var bool */
|
|
||||||
private $classMapAuthoritative = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array<string, bool>
|
|
||||||
*/
|
|
||||||
private $missingClasses = array();
|
|
||||||
|
|
||||||
/** @var string|null */
|
|
||||||
private $apcuPrefix;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array<string, self>
|
|
||||||
*/
|
|
||||||
private static $registeredLoaders = array();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string|null $vendorDir
|
|
||||||
*/
|
|
||||||
public function __construct($vendorDir = null)
|
|
||||||
{
|
|
||||||
$this->vendorDir = $vendorDir;
|
|
||||||
self::initializeIncludeClosure();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, list<string>>
|
|
||||||
*/
|
|
||||||
public function getPrefixes()
|
|
||||||
{
|
|
||||||
if (!empty($this->prefixesPsr0)) {
|
|
||||||
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
|
|
||||||
}
|
|
||||||
|
|
||||||
return array();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, list<string>>
|
|
||||||
*/
|
|
||||||
public function getPrefixesPsr4()
|
|
||||||
{
|
|
||||||
return $this->prefixDirsPsr4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<string>
|
|
||||||
*/
|
|
||||||
public function getFallbackDirs()
|
|
||||||
{
|
|
||||||
return $this->fallbackDirsPsr0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<string>
|
|
||||||
*/
|
|
||||||
public function getFallbackDirsPsr4()
|
|
||||||
{
|
|
||||||
return $this->fallbackDirsPsr4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string> Array of classname => path
|
|
||||||
*/
|
|
||||||
public function getClassMap()
|
|
||||||
{
|
|
||||||
return $this->classMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, string> $classMap Class to filename map
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function addClassMap(array $classMap)
|
|
||||||
{
|
|
||||||
if ($this->classMap) {
|
|
||||||
$this->classMap = array_merge($this->classMap, $classMap);
|
|
||||||
} else {
|
|
||||||
$this->classMap = $classMap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a set of PSR-0 directories for a given prefix, either
|
|
||||||
* appending or prepending to the ones previously set for this prefix.
|
|
||||||
*
|
|
||||||
* @param string $prefix The prefix
|
|
||||||
* @param list<string>|string $paths The PSR-0 root directories
|
|
||||||
* @param bool $prepend Whether to prepend the directories
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function add($prefix, $paths, $prepend = false)
|
|
||||||
{
|
|
||||||
$paths = (array) $paths;
|
|
||||||
if (!$prefix) {
|
|
||||||
if ($prepend) {
|
|
||||||
$this->fallbackDirsPsr0 = array_merge(
|
|
||||||
$paths,
|
|
||||||
$this->fallbackDirsPsr0
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
$this->fallbackDirsPsr0 = array_merge(
|
|
||||||
$this->fallbackDirsPsr0,
|
|
||||||
$paths
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$first = $prefix[0];
|
|
||||||
if (!isset($this->prefixesPsr0[$first][$prefix])) {
|
|
||||||
$this->prefixesPsr0[$first][$prefix] = $paths;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ($prepend) {
|
|
||||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
|
||||||
$paths,
|
|
||||||
$this->prefixesPsr0[$first][$prefix]
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
|
||||||
$this->prefixesPsr0[$first][$prefix],
|
|
||||||
$paths
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a set of PSR-4 directories for a given namespace, either
|
|
||||||
* appending or prepending to the ones previously set for this namespace.
|
|
||||||
*
|
|
||||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
|
||||||
* @param list<string>|string $paths The PSR-4 base directories
|
|
||||||
* @param bool $prepend Whether to prepend the directories
|
|
||||||
*
|
|
||||||
* @throws \InvalidArgumentException
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function addPsr4($prefix, $paths, $prepend = false)
|
|
||||||
{
|
|
||||||
$paths = (array) $paths;
|
|
||||||
if (!$prefix) {
|
|
||||||
// Register directories for the root namespace.
|
|
||||||
if ($prepend) {
|
|
||||||
$this->fallbackDirsPsr4 = array_merge(
|
|
||||||
$paths,
|
|
||||||
$this->fallbackDirsPsr4
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
$this->fallbackDirsPsr4 = array_merge(
|
|
||||||
$this->fallbackDirsPsr4,
|
|
||||||
$paths
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
|
|
||||||
// Register directories for a new namespace.
|
|
||||||
$length = strlen($prefix);
|
|
||||||
if ('\\' !== $prefix[$length - 1]) {
|
|
||||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
|
||||||
}
|
|
||||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
|
||||||
$this->prefixDirsPsr4[$prefix] = $paths;
|
|
||||||
} elseif ($prepend) {
|
|
||||||
// Prepend directories for an already registered namespace.
|
|
||||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
|
||||||
$paths,
|
|
||||||
$this->prefixDirsPsr4[$prefix]
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Append directories for an already registered namespace.
|
|
||||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
|
||||||
$this->prefixDirsPsr4[$prefix],
|
|
||||||
$paths
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a set of PSR-0 directories for a given prefix,
|
|
||||||
* replacing any others previously set for this prefix.
|
|
||||||
*
|
|
||||||
* @param string $prefix The prefix
|
|
||||||
* @param list<string>|string $paths The PSR-0 base directories
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function set($prefix, $paths)
|
|
||||||
{
|
|
||||||
if (!$prefix) {
|
|
||||||
$this->fallbackDirsPsr0 = (array) $paths;
|
|
||||||
} else {
|
|
||||||
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a set of PSR-4 directories for a given namespace,
|
|
||||||
* replacing any others previously set for this namespace.
|
|
||||||
*
|
|
||||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
|
||||||
* @param list<string>|string $paths The PSR-4 base directories
|
|
||||||
*
|
|
||||||
* @throws \InvalidArgumentException
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function setPsr4($prefix, $paths)
|
|
||||||
{
|
|
||||||
if (!$prefix) {
|
|
||||||
$this->fallbackDirsPsr4 = (array) $paths;
|
|
||||||
} else {
|
|
||||||
$length = strlen($prefix);
|
|
||||||
if ('\\' !== $prefix[$length - 1]) {
|
|
||||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
|
||||||
}
|
|
||||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
|
||||||
$this->prefixDirsPsr4[$prefix] = (array) $paths;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Turns on searching the include path for class files.
|
|
||||||
*
|
|
||||||
* @param bool $useIncludePath
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function setUseIncludePath($useIncludePath)
|
|
||||||
{
|
|
||||||
$this->useIncludePath = $useIncludePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Can be used to check if the autoloader uses the include path to check
|
|
||||||
* for classes.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function getUseIncludePath()
|
|
||||||
{
|
|
||||||
return $this->useIncludePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Turns off searching the prefix and fallback directories for classes
|
|
||||||
* that have not been registered with the class map.
|
|
||||||
*
|
|
||||||
* @param bool $classMapAuthoritative
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function setClassMapAuthoritative($classMapAuthoritative)
|
|
||||||
{
|
|
||||||
$this->classMapAuthoritative = $classMapAuthoritative;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Should class lookup fail if not found in the current class map?
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isClassMapAuthoritative()
|
|
||||||
{
|
|
||||||
return $this->classMapAuthoritative;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
|
|
||||||
*
|
|
||||||
* @param string|null $apcuPrefix
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function setApcuPrefix($apcuPrefix)
|
|
||||||
{
|
|
||||||
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The APCu prefix in use, or null if APCu caching is not enabled.
|
|
||||||
*
|
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
public function getApcuPrefix()
|
|
||||||
{
|
|
||||||
return $this->apcuPrefix;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers this instance as an autoloader.
|
|
||||||
*
|
|
||||||
* @param bool $prepend Whether to prepend the autoloader or not
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function register($prepend = false)
|
|
||||||
{
|
|
||||||
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
|
|
||||||
|
|
||||||
if (null === $this->vendorDir) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($prepend) {
|
|
||||||
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
|
|
||||||
} else {
|
|
||||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
|
||||||
self::$registeredLoaders[$this->vendorDir] = $this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unregisters this instance as an autoloader.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function unregister()
|
|
||||||
{
|
|
||||||
spl_autoload_unregister(array($this, 'loadClass'));
|
|
||||||
|
|
||||||
if (null !== $this->vendorDir) {
|
|
||||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the given class or interface.
|
|
||||||
*
|
|
||||||
* @param string $class The name of the class
|
|
||||||
* @return true|null True if loaded, null otherwise
|
|
||||||
*/
|
|
||||||
public function loadClass($class)
|
|
||||||
{
|
|
||||||
if ($file = $this->findFile($class)) {
|
|
||||||
$includeFile = self::$includeFile;
|
|
||||||
$includeFile($file);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the path to the file where the class is defined.
|
|
||||||
*
|
|
||||||
* @param string $class The name of the class
|
|
||||||
*
|
|
||||||
* @return string|false The path if found, false otherwise
|
|
||||||
*/
|
|
||||||
public function findFile($class)
|
|
||||||
{
|
|
||||||
// class map lookup
|
|
||||||
if (isset($this->classMap[$class])) {
|
|
||||||
return $this->classMap[$class];
|
|
||||||
}
|
|
||||||
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (null !== $this->apcuPrefix) {
|
|
||||||
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
|
|
||||||
if ($hit) {
|
|
||||||
return $file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$file = $this->findFileWithExtension($class, '.php');
|
|
||||||
|
|
||||||
// Search for Hack files if we are running on HHVM
|
|
||||||
if (false === $file && defined('HHVM_VERSION')) {
|
|
||||||
$file = $this->findFileWithExtension($class, '.hh');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null !== $this->apcuPrefix) {
|
|
||||||
apcu_add($this->apcuPrefix.$class, $file);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (false === $file) {
|
|
||||||
// Remember that this class does not exist.
|
|
||||||
$this->missingClasses[$class] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $file;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the currently registered loaders keyed by their corresponding vendor directories.
|
|
||||||
*
|
|
||||||
* @return array<string, self>
|
|
||||||
*/
|
|
||||||
public static function getRegisteredLoaders()
|
|
||||||
{
|
|
||||||
return self::$registeredLoaders;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $class
|
|
||||||
* @param string $ext
|
|
||||||
* @return string|false
|
|
||||||
*/
|
|
||||||
private function findFileWithExtension($class, $ext)
|
|
||||||
{
|
|
||||||
// PSR-4 lookup
|
|
||||||
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
|
|
||||||
|
|
||||||
$first = $class[0];
|
|
||||||
if (isset($this->prefixLengthsPsr4[$first])) {
|
|
||||||
$subPath = $class;
|
|
||||||
while (false !== $lastPos = strrpos($subPath, '\\')) {
|
|
||||||
$subPath = substr($subPath, 0, $lastPos);
|
|
||||||
$search = $subPath . '\\';
|
|
||||||
if (isset($this->prefixDirsPsr4[$search])) {
|
|
||||||
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
|
|
||||||
foreach ($this->prefixDirsPsr4[$search] as $dir) {
|
|
||||||
if (file_exists($file = $dir . $pathEnd)) {
|
|
||||||
return $file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PSR-4 fallback dirs
|
|
||||||
foreach ($this->fallbackDirsPsr4 as $dir) {
|
|
||||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
|
|
||||||
return $file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PSR-0 lookup
|
|
||||||
if (false !== $pos = strrpos($class, '\\')) {
|
|
||||||
// namespaced class name
|
|
||||||
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
|
|
||||||
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
|
|
||||||
} else {
|
|
||||||
// PEAR-like class name
|
|
||||||
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($this->prefixesPsr0[$first])) {
|
|
||||||
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
|
|
||||||
if (0 === strpos($class, $prefix)) {
|
|
||||||
foreach ($dirs as $dir) {
|
|
||||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
|
||||||
return $file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PSR-0 fallback dirs
|
|
||||||
foreach ($this->fallbackDirsPsr0 as $dir) {
|
|
||||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
|
||||||
return $file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PSR-0 include paths.
|
|
||||||
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
|
|
||||||
return $file;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
private static function initializeIncludeClosure()
|
|
||||||
{
|
|
||||||
if (self::$includeFile !== null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scope isolated include.
|
|
||||||
*
|
|
||||||
* Prevents access to $this/self from included files.
|
|
||||||
*
|
|
||||||
* @param string $file
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
self::$includeFile = \Closure::bind(static function($file) {
|
|
||||||
include $file;
|
|
||||||
}, null, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
359
pirp/vendor/composer/InstalledVersions.php
vendored
359
pirp/vendor/composer/InstalledVersions.php
vendored
@@ -1,359 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of Composer.
|
|
||||||
*
|
|
||||||
* (c) Nils Adermann <naderman@naderman.de>
|
|
||||||
* Jordi Boggiano <j.boggiano@seld.be>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Composer;
|
|
||||||
|
|
||||||
use Composer\Autoload\ClassLoader;
|
|
||||||
use Composer\Semver\VersionParser;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is copied in every Composer installed project and available to all
|
|
||||||
*
|
|
||||||
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
|
|
||||||
*
|
|
||||||
* To require its presence, you can require `composer-runtime-api ^2.0`
|
|
||||||
*
|
|
||||||
* @final
|
|
||||||
*/
|
|
||||||
class InstalledVersions
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var mixed[]|null
|
|
||||||
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
|
|
||||||
*/
|
|
||||||
private static $installed;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var bool|null
|
|
||||||
*/
|
|
||||||
private static $canGetVendors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array[]
|
|
||||||
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
|
||||||
*/
|
|
||||||
private static $installedByVendor = array();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list of all package names which are present, either by being installed, replaced or provided
|
|
||||||
*
|
|
||||||
* @return string[]
|
|
||||||
* @psalm-return list<string>
|
|
||||||
*/
|
|
||||||
public static function getInstalledPackages()
|
|
||||||
{
|
|
||||||
$packages = array();
|
|
||||||
foreach (self::getInstalled() as $installed) {
|
|
||||||
$packages[] = array_keys($installed['versions']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (1 === \count($packages)) {
|
|
||||||
return $packages[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list of all package names with a specific type e.g. 'library'
|
|
||||||
*
|
|
||||||
* @param string $type
|
|
||||||
* @return string[]
|
|
||||||
* @psalm-return list<string>
|
|
||||||
*/
|
|
||||||
public static function getInstalledPackagesByType($type)
|
|
||||||
{
|
|
||||||
$packagesByType = array();
|
|
||||||
|
|
||||||
foreach (self::getInstalled() as $installed) {
|
|
||||||
foreach ($installed['versions'] as $name => $package) {
|
|
||||||
if (isset($package['type']) && $package['type'] === $type) {
|
|
||||||
$packagesByType[] = $name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $packagesByType;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the given package is installed
|
|
||||||
*
|
|
||||||
* This also returns true if the package name is provided or replaced by another package
|
|
||||||
*
|
|
||||||
* @param string $packageName
|
|
||||||
* @param bool $includeDevRequirements
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function isInstalled($packageName, $includeDevRequirements = true)
|
|
||||||
{
|
|
||||||
foreach (self::getInstalled() as $installed) {
|
|
||||||
if (isset($installed['versions'][$packageName])) {
|
|
||||||
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the given package satisfies a version constraint
|
|
||||||
*
|
|
||||||
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
|
|
||||||
*
|
|
||||||
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
|
|
||||||
*
|
|
||||||
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
|
|
||||||
* @param string $packageName
|
|
||||||
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function satisfies(VersionParser $parser, $packageName, $constraint)
|
|
||||||
{
|
|
||||||
$constraint = $parser->parseConstraints((string) $constraint);
|
|
||||||
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
|
|
||||||
|
|
||||||
return $provided->matches($constraint);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a version constraint representing all the range(s) which are installed for a given package
|
|
||||||
*
|
|
||||||
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
|
|
||||||
* whether a given version of a package is installed, and not just whether it exists
|
|
||||||
*
|
|
||||||
* @param string $packageName
|
|
||||||
* @return string Version constraint usable with composer/semver
|
|
||||||
*/
|
|
||||||
public static function getVersionRanges($packageName)
|
|
||||||
{
|
|
||||||
foreach (self::getInstalled() as $installed) {
|
|
||||||
if (!isset($installed['versions'][$packageName])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$ranges = array();
|
|
||||||
if (isset($installed['versions'][$packageName]['pretty_version'])) {
|
|
||||||
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
|
|
||||||
}
|
|
||||||
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
|
|
||||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
|
|
||||||
}
|
|
||||||
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
|
|
||||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
|
|
||||||
}
|
|
||||||
if (array_key_exists('provided', $installed['versions'][$packageName])) {
|
|
||||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
|
|
||||||
}
|
|
||||||
|
|
||||||
return implode(' || ', $ranges);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $packageName
|
|
||||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
|
||||||
*/
|
|
||||||
public static function getVersion($packageName)
|
|
||||||
{
|
|
||||||
foreach (self::getInstalled() as $installed) {
|
|
||||||
if (!isset($installed['versions'][$packageName])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($installed['versions'][$packageName]['version'])) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $installed['versions'][$packageName]['version'];
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $packageName
|
|
||||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
|
||||||
*/
|
|
||||||
public static function getPrettyVersion($packageName)
|
|
||||||
{
|
|
||||||
foreach (self::getInstalled() as $installed) {
|
|
||||||
if (!isset($installed['versions'][$packageName])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $installed['versions'][$packageName]['pretty_version'];
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $packageName
|
|
||||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
|
|
||||||
*/
|
|
||||||
public static function getReference($packageName)
|
|
||||||
{
|
|
||||||
foreach (self::getInstalled() as $installed) {
|
|
||||||
if (!isset($installed['versions'][$packageName])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($installed['versions'][$packageName]['reference'])) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $installed['versions'][$packageName]['reference'];
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $packageName
|
|
||||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
|
|
||||||
*/
|
|
||||||
public static function getInstallPath($packageName)
|
|
||||||
{
|
|
||||||
foreach (self::getInstalled() as $installed) {
|
|
||||||
if (!isset($installed['versions'][$packageName])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array
|
|
||||||
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
|
|
||||||
*/
|
|
||||||
public static function getRootPackage()
|
|
||||||
{
|
|
||||||
$installed = self::getInstalled();
|
|
||||||
|
|
||||||
return $installed[0]['root'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the raw installed.php data for custom implementations
|
|
||||||
*
|
|
||||||
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
|
|
||||||
* @return array[]
|
|
||||||
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
|
|
||||||
*/
|
|
||||||
public static function getRawData()
|
|
||||||
{
|
|
||||||
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
|
|
||||||
|
|
||||||
if (null === self::$installed) {
|
|
||||||
// only require the installed.php file if this file is loaded from its dumped location,
|
|
||||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
|
||||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
|
||||||
self::$installed = include __DIR__ . '/installed.php';
|
|
||||||
} else {
|
|
||||||
self::$installed = array();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return self::$installed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the raw data of all installed.php which are currently loaded for custom implementations
|
|
||||||
*
|
|
||||||
* @return array[]
|
|
||||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
|
||||||
*/
|
|
||||||
public static function getAllRawData()
|
|
||||||
{
|
|
||||||
return self::getInstalled();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lets you reload the static array from another file
|
|
||||||
*
|
|
||||||
* This is only useful for complex integrations in which a project needs to use
|
|
||||||
* this class but then also needs to execute another project's autoloader in process,
|
|
||||||
* and wants to ensure both projects have access to their version of installed.php.
|
|
||||||
*
|
|
||||||
* A typical case would be PHPUnit, where it would need to make sure it reads all
|
|
||||||
* the data it needs from this class, then call reload() with
|
|
||||||
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
|
|
||||||
* the project in which it runs can then also use this class safely, without
|
|
||||||
* interference between PHPUnit's dependencies and the project's dependencies.
|
|
||||||
*
|
|
||||||
* @param array[] $data A vendor/composer/installed.php data set
|
|
||||||
* @return void
|
|
||||||
*
|
|
||||||
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
|
|
||||||
*/
|
|
||||||
public static function reload($data)
|
|
||||||
{
|
|
||||||
self::$installed = $data;
|
|
||||||
self::$installedByVendor = array();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array[]
|
|
||||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
|
||||||
*/
|
|
||||||
private static function getInstalled()
|
|
||||||
{
|
|
||||||
if (null === self::$canGetVendors) {
|
|
||||||
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
|
|
||||||
}
|
|
||||||
|
|
||||||
$installed = array();
|
|
||||||
|
|
||||||
if (self::$canGetVendors) {
|
|
||||||
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
|
||||||
if (isset(self::$installedByVendor[$vendorDir])) {
|
|
||||||
$installed[] = self::$installedByVendor[$vendorDir];
|
|
||||||
} elseif (is_file($vendorDir.'/composer/installed.php')) {
|
|
||||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
|
||||||
$required = require $vendorDir.'/composer/installed.php';
|
|
||||||
$installed[] = self::$installedByVendor[$vendorDir] = $required;
|
|
||||||
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
|
|
||||||
self::$installed = $installed[count($installed) - 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null === self::$installed) {
|
|
||||||
// only require the installed.php file if this file is loaded from its dumped location,
|
|
||||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
|
||||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
|
||||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
|
||||||
$required = require __DIR__ . '/installed.php';
|
|
||||||
self::$installed = $required;
|
|
||||||
} else {
|
|
||||||
self::$installed = array();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self::$installed !== array()) {
|
|
||||||
$installed[] = self::$installed;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $installed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
19
pirp/vendor/composer/LICENSE
vendored
19
pirp/vendor/composer/LICENSE
vendored
@@ -1,19 +0,0 @@
|
|||||||
Copyright (c) Nils Adermann, Jordi Boggiano
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is furnished
|
|
||||||
to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
209
pirp/vendor/composer/autoload_classmap.php
vendored
209
pirp/vendor/composer/autoload_classmap.php
vendored
@@ -1,209 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
// autoload_classmap.php @generated by Composer
|
|
||||||
|
|
||||||
$vendorDir = dirname(__DIR__);
|
|
||||||
$baseDir = dirname($vendorDir);
|
|
||||||
|
|
||||||
return array(
|
|
||||||
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
|
|
||||||
'Dompdf\\Adapter\\CPDF' => $vendorDir . '/dompdf/dompdf/src/Adapter/CPDF.php',
|
|
||||||
'Dompdf\\Adapter\\GD' => $vendorDir . '/dompdf/dompdf/src/Adapter/GD.php',
|
|
||||||
'Dompdf\\Adapter\\PDFLib' => $vendorDir . '/dompdf/dompdf/src/Adapter/PDFLib.php',
|
|
||||||
'Dompdf\\Canvas' => $vendorDir . '/dompdf/dompdf/src/Canvas.php',
|
|
||||||
'Dompdf\\CanvasFactory' => $vendorDir . '/dompdf/dompdf/src/CanvasFactory.php',
|
|
||||||
'Dompdf\\Cellmap' => $vendorDir . '/dompdf/dompdf/src/Cellmap.php',
|
|
||||||
'Dompdf\\Cpdf' => $vendorDir . '/dompdf/dompdf/lib/Cpdf.php',
|
|
||||||
'Dompdf\\Css\\AttributeTranslator' => $vendorDir . '/dompdf/dompdf/src/Css/AttributeTranslator.php',
|
|
||||||
'Dompdf\\Css\\Color' => $vendorDir . '/dompdf/dompdf/src/Css/Color.php',
|
|
||||||
'Dompdf\\Css\\Style' => $vendorDir . '/dompdf/dompdf/src/Css/Style.php',
|
|
||||||
'Dompdf\\Css\\Stylesheet' => $vendorDir . '/dompdf/dompdf/src/Css/Stylesheet.php',
|
|
||||||
'Dompdf\\Dompdf' => $vendorDir . '/dompdf/dompdf/src/Dompdf.php',
|
|
||||||
'Dompdf\\Exception' => $vendorDir . '/dompdf/dompdf/src/Exception.php',
|
|
||||||
'Dompdf\\Exception\\ImageException' => $vendorDir . '/dompdf/dompdf/src/Exception/ImageException.php',
|
|
||||||
'Dompdf\\FontMetrics' => $vendorDir . '/dompdf/dompdf/src/FontMetrics.php',
|
|
||||||
'Dompdf\\Frame' => $vendorDir . '/dompdf/dompdf/src/Frame.php',
|
|
||||||
'Dompdf\\FrameDecorator\\AbstractFrameDecorator' => $vendorDir . '/dompdf/dompdf/src/FrameDecorator/AbstractFrameDecorator.php',
|
|
||||||
'Dompdf\\FrameDecorator\\Block' => $vendorDir . '/dompdf/dompdf/src/FrameDecorator/Block.php',
|
|
||||||
'Dompdf\\FrameDecorator\\Image' => $vendorDir . '/dompdf/dompdf/src/FrameDecorator/Image.php',
|
|
||||||
'Dompdf\\FrameDecorator\\Inline' => $vendorDir . '/dompdf/dompdf/src/FrameDecorator/Inline.php',
|
|
||||||
'Dompdf\\FrameDecorator\\ListBullet' => $vendorDir . '/dompdf/dompdf/src/FrameDecorator/ListBullet.php',
|
|
||||||
'Dompdf\\FrameDecorator\\ListBulletImage' => $vendorDir . '/dompdf/dompdf/src/FrameDecorator/ListBulletImage.php',
|
|
||||||
'Dompdf\\FrameDecorator\\NullFrameDecorator' => $vendorDir . '/dompdf/dompdf/src/FrameDecorator/NullFrameDecorator.php',
|
|
||||||
'Dompdf\\FrameDecorator\\Page' => $vendorDir . '/dompdf/dompdf/src/FrameDecorator/Page.php',
|
|
||||||
'Dompdf\\FrameDecorator\\Table' => $vendorDir . '/dompdf/dompdf/src/FrameDecorator/Table.php',
|
|
||||||
'Dompdf\\FrameDecorator\\TableCell' => $vendorDir . '/dompdf/dompdf/src/FrameDecorator/TableCell.php',
|
|
||||||
'Dompdf\\FrameDecorator\\TableRow' => $vendorDir . '/dompdf/dompdf/src/FrameDecorator/TableRow.php',
|
|
||||||
'Dompdf\\FrameDecorator\\TableRowGroup' => $vendorDir . '/dompdf/dompdf/src/FrameDecorator/TableRowGroup.php',
|
|
||||||
'Dompdf\\FrameDecorator\\Text' => $vendorDir . '/dompdf/dompdf/src/FrameDecorator/Text.php',
|
|
||||||
'Dompdf\\FrameReflower\\AbstractFrameReflower' => $vendorDir . '/dompdf/dompdf/src/FrameReflower/AbstractFrameReflower.php',
|
|
||||||
'Dompdf\\FrameReflower\\Block' => $vendorDir . '/dompdf/dompdf/src/FrameReflower/Block.php',
|
|
||||||
'Dompdf\\FrameReflower\\Image' => $vendorDir . '/dompdf/dompdf/src/FrameReflower/Image.php',
|
|
||||||
'Dompdf\\FrameReflower\\Inline' => $vendorDir . '/dompdf/dompdf/src/FrameReflower/Inline.php',
|
|
||||||
'Dompdf\\FrameReflower\\ListBullet' => $vendorDir . '/dompdf/dompdf/src/FrameReflower/ListBullet.php',
|
|
||||||
'Dompdf\\FrameReflower\\NullFrameReflower' => $vendorDir . '/dompdf/dompdf/src/FrameReflower/NullFrameReflower.php',
|
|
||||||
'Dompdf\\FrameReflower\\Page' => $vendorDir . '/dompdf/dompdf/src/FrameReflower/Page.php',
|
|
||||||
'Dompdf\\FrameReflower\\Table' => $vendorDir . '/dompdf/dompdf/src/FrameReflower/Table.php',
|
|
||||||
'Dompdf\\FrameReflower\\TableCell' => $vendorDir . '/dompdf/dompdf/src/FrameReflower/TableCell.php',
|
|
||||||
'Dompdf\\FrameReflower\\TableRow' => $vendorDir . '/dompdf/dompdf/src/FrameReflower/TableRow.php',
|
|
||||||
'Dompdf\\FrameReflower\\TableRowGroup' => $vendorDir . '/dompdf/dompdf/src/FrameReflower/TableRowGroup.php',
|
|
||||||
'Dompdf\\FrameReflower\\Text' => $vendorDir . '/dompdf/dompdf/src/FrameReflower/Text.php',
|
|
||||||
'Dompdf\\Frame\\Factory' => $vendorDir . '/dompdf/dompdf/src/Frame/Factory.php',
|
|
||||||
'Dompdf\\Frame\\FrameListIterator' => $vendorDir . '/dompdf/dompdf/src/Frame/FrameListIterator.php',
|
|
||||||
'Dompdf\\Frame\\FrameTree' => $vendorDir . '/dompdf/dompdf/src/Frame/FrameTree.php',
|
|
||||||
'Dompdf\\Frame\\FrameTreeIterator' => $vendorDir . '/dompdf/dompdf/src/Frame/FrameTreeIterator.php',
|
|
||||||
'Dompdf\\Helpers' => $vendorDir . '/dompdf/dompdf/src/Helpers.php',
|
|
||||||
'Dompdf\\Image\\Cache' => $vendorDir . '/dompdf/dompdf/src/Image/Cache.php',
|
|
||||||
'Dompdf\\JavascriptEmbedder' => $vendorDir . '/dompdf/dompdf/src/JavascriptEmbedder.php',
|
|
||||||
'Dompdf\\LineBox' => $vendorDir . '/dompdf/dompdf/src/LineBox.php',
|
|
||||||
'Dompdf\\Options' => $vendorDir . '/dompdf/dompdf/src/Options.php',
|
|
||||||
'Dompdf\\PhpEvaluator' => $vendorDir . '/dompdf/dompdf/src/PhpEvaluator.php',
|
|
||||||
'Dompdf\\Positioner\\Absolute' => $vendorDir . '/dompdf/dompdf/src/Positioner/Absolute.php',
|
|
||||||
'Dompdf\\Positioner\\AbstractPositioner' => $vendorDir . '/dompdf/dompdf/src/Positioner/AbstractPositioner.php',
|
|
||||||
'Dompdf\\Positioner\\Block' => $vendorDir . '/dompdf/dompdf/src/Positioner/Block.php',
|
|
||||||
'Dompdf\\Positioner\\Fixed' => $vendorDir . '/dompdf/dompdf/src/Positioner/Fixed.php',
|
|
||||||
'Dompdf\\Positioner\\Inline' => $vendorDir . '/dompdf/dompdf/src/Positioner/Inline.php',
|
|
||||||
'Dompdf\\Positioner\\ListBullet' => $vendorDir . '/dompdf/dompdf/src/Positioner/ListBullet.php',
|
|
||||||
'Dompdf\\Positioner\\NullPositioner' => $vendorDir . '/dompdf/dompdf/src/Positioner/NullPositioner.php',
|
|
||||||
'Dompdf\\Positioner\\TableCell' => $vendorDir . '/dompdf/dompdf/src/Positioner/TableCell.php',
|
|
||||||
'Dompdf\\Positioner\\TableRow' => $vendorDir . '/dompdf/dompdf/src/Positioner/TableRow.php',
|
|
||||||
'Dompdf\\Renderer' => $vendorDir . '/dompdf/dompdf/src/Renderer.php',
|
|
||||||
'Dompdf\\Renderer\\AbstractRenderer' => $vendorDir . '/dompdf/dompdf/src/Renderer/AbstractRenderer.php',
|
|
||||||
'Dompdf\\Renderer\\Block' => $vendorDir . '/dompdf/dompdf/src/Renderer/Block.php',
|
|
||||||
'Dompdf\\Renderer\\Image' => $vendorDir . '/dompdf/dompdf/src/Renderer/Image.php',
|
|
||||||
'Dompdf\\Renderer\\Inline' => $vendorDir . '/dompdf/dompdf/src/Renderer/Inline.php',
|
|
||||||
'Dompdf\\Renderer\\ListBullet' => $vendorDir . '/dompdf/dompdf/src/Renderer/ListBullet.php',
|
|
||||||
'Dompdf\\Renderer\\TableCell' => $vendorDir . '/dompdf/dompdf/src/Renderer/TableCell.php',
|
|
||||||
'Dompdf\\Renderer\\TableRowGroup' => $vendorDir . '/dompdf/dompdf/src/Renderer/TableRowGroup.php',
|
|
||||||
'Dompdf\\Renderer\\Text' => $vendorDir . '/dompdf/dompdf/src/Renderer/Text.php',
|
|
||||||
'FontLib\\AdobeFontMetrics' => $vendorDir . '/phenx/php-font-lib/src/FontLib/AdobeFontMetrics.php',
|
|
||||||
'FontLib\\BinaryStream' => $vendorDir . '/phenx/php-font-lib/src/FontLib/BinaryStream.php',
|
|
||||||
'FontLib\\EOT\\File' => $vendorDir . '/phenx/php-font-lib/src/FontLib/EOT/File.php',
|
|
||||||
'FontLib\\EOT\\Header' => $vendorDir . '/phenx/php-font-lib/src/FontLib/EOT/Header.php',
|
|
||||||
'FontLib\\EncodingMap' => $vendorDir . '/phenx/php-font-lib/src/FontLib/EncodingMap.php',
|
|
||||||
'FontLib\\Exception\\FontNotFoundException' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Exception/FontNotFoundException.php',
|
|
||||||
'FontLib\\Font' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Font.php',
|
|
||||||
'FontLib\\Glyph\\Outline' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Glyph/Outline.php',
|
|
||||||
'FontLib\\Glyph\\OutlineComponent' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Glyph/OutlineComponent.php',
|
|
||||||
'FontLib\\Glyph\\OutlineComposite' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Glyph/OutlineComposite.php',
|
|
||||||
'FontLib\\Glyph\\OutlineSimple' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Glyph/OutlineSimple.php',
|
|
||||||
'FontLib\\Header' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Header.php',
|
|
||||||
'FontLib\\OpenType\\File' => $vendorDir . '/phenx/php-font-lib/src/FontLib/OpenType/File.php',
|
|
||||||
'FontLib\\OpenType\\TableDirectoryEntry' => $vendorDir . '/phenx/php-font-lib/src/FontLib/OpenType/TableDirectoryEntry.php',
|
|
||||||
'FontLib\\Table\\DirectoryEntry' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Table/DirectoryEntry.php',
|
|
||||||
'FontLib\\Table\\Table' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Table/Table.php',
|
|
||||||
'FontLib\\Table\\Type\\cmap' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Table/Type/cmap.php',
|
|
||||||
'FontLib\\Table\\Type\\cvt' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Table/Type/cvt.php',
|
|
||||||
'FontLib\\Table\\Type\\fpgm' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Table/Type/fpgm.php',
|
|
||||||
'FontLib\\Table\\Type\\glyf' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Table/Type/glyf.php',
|
|
||||||
'FontLib\\Table\\Type\\head' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Table/Type/head.php',
|
|
||||||
'FontLib\\Table\\Type\\hhea' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Table/Type/hhea.php',
|
|
||||||
'FontLib\\Table\\Type\\hmtx' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Table/Type/hmtx.php',
|
|
||||||
'FontLib\\Table\\Type\\kern' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Table/Type/kern.php',
|
|
||||||
'FontLib\\Table\\Type\\loca' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Table/Type/loca.php',
|
|
||||||
'FontLib\\Table\\Type\\maxp' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Table/Type/maxp.php',
|
|
||||||
'FontLib\\Table\\Type\\name' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Table/Type/name.php',
|
|
||||||
'FontLib\\Table\\Type\\nameRecord' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Table/Type/nameRecord.php',
|
|
||||||
'FontLib\\Table\\Type\\os2' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Table/Type/os2.php',
|
|
||||||
'FontLib\\Table\\Type\\post' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Table/Type/post.php',
|
|
||||||
'FontLib\\Table\\Type\\prep' => $vendorDir . '/phenx/php-font-lib/src/FontLib/Table/Type/prep.php',
|
|
||||||
'FontLib\\TrueType\\Collection' => $vendorDir . '/phenx/php-font-lib/src/FontLib/TrueType/Collection.php',
|
|
||||||
'FontLib\\TrueType\\File' => $vendorDir . '/phenx/php-font-lib/src/FontLib/TrueType/File.php',
|
|
||||||
'FontLib\\TrueType\\Header' => $vendorDir . '/phenx/php-font-lib/src/FontLib/TrueType/Header.php',
|
|
||||||
'FontLib\\TrueType\\TableDirectoryEntry' => $vendorDir . '/phenx/php-font-lib/src/FontLib/TrueType/TableDirectoryEntry.php',
|
|
||||||
'FontLib\\WOFF\\File' => $vendorDir . '/phenx/php-font-lib/src/FontLib/WOFF/File.php',
|
|
||||||
'FontLib\\WOFF\\Header' => $vendorDir . '/phenx/php-font-lib/src/FontLib/WOFF/Header.php',
|
|
||||||
'FontLib\\WOFF\\TableDirectoryEntry' => $vendorDir . '/phenx/php-font-lib/src/FontLib/WOFF/TableDirectoryEntry.php',
|
|
||||||
'Masterminds\\HTML5' => $vendorDir . '/masterminds/html5/src/HTML5.php',
|
|
||||||
'Masterminds\\HTML5\\Elements' => $vendorDir . '/masterminds/html5/src/HTML5/Elements.php',
|
|
||||||
'Masterminds\\HTML5\\Entities' => $vendorDir . '/masterminds/html5/src/HTML5/Entities.php',
|
|
||||||
'Masterminds\\HTML5\\Exception' => $vendorDir . '/masterminds/html5/src/HTML5/Exception.php',
|
|
||||||
'Masterminds\\HTML5\\InstructionProcessor' => $vendorDir . '/masterminds/html5/src/HTML5/InstructionProcessor.php',
|
|
||||||
'Masterminds\\HTML5\\Parser\\CharacterReference' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/CharacterReference.php',
|
|
||||||
'Masterminds\\HTML5\\Parser\\DOMTreeBuilder' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/DOMTreeBuilder.php',
|
|
||||||
'Masterminds\\HTML5\\Parser\\EventHandler' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/EventHandler.php',
|
|
||||||
'Masterminds\\HTML5\\Parser\\FileInputStream' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/FileInputStream.php',
|
|
||||||
'Masterminds\\HTML5\\Parser\\InputStream' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/InputStream.php',
|
|
||||||
'Masterminds\\HTML5\\Parser\\ParseError' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/ParseError.php',
|
|
||||||
'Masterminds\\HTML5\\Parser\\Scanner' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/Scanner.php',
|
|
||||||
'Masterminds\\HTML5\\Parser\\StringInputStream' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/StringInputStream.php',
|
|
||||||
'Masterminds\\HTML5\\Parser\\Tokenizer' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/Tokenizer.php',
|
|
||||||
'Masterminds\\HTML5\\Parser\\TreeBuildingRules' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/TreeBuildingRules.php',
|
|
||||||
'Masterminds\\HTML5\\Parser\\UTF8Utils' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/UTF8Utils.php',
|
|
||||||
'Masterminds\\HTML5\\Serializer\\HTML5Entities' => $vendorDir . '/masterminds/html5/src/HTML5/Serializer/HTML5Entities.php',
|
|
||||||
'Masterminds\\HTML5\\Serializer\\OutputRules' => $vendorDir . '/masterminds/html5/src/HTML5/Serializer/OutputRules.php',
|
|
||||||
'Masterminds\\HTML5\\Serializer\\RulesInterface' => $vendorDir . '/masterminds/html5/src/HTML5/Serializer/RulesInterface.php',
|
|
||||||
'Masterminds\\HTML5\\Serializer\\Traverser' => $vendorDir . '/masterminds/html5/src/HTML5/Serializer/Traverser.php',
|
|
||||||
'Sabberworm\\CSS\\CSSElement' => $vendorDir . '/sabberworm/php-css-parser/src/CSSElement.php',
|
|
||||||
'Sabberworm\\CSS\\CSSList\\AtRuleBlockList' => $vendorDir . '/sabberworm/php-css-parser/src/CSSList/AtRuleBlockList.php',
|
|
||||||
'Sabberworm\\CSS\\CSSList\\CSSBlockList' => $vendorDir . '/sabberworm/php-css-parser/src/CSSList/CSSBlockList.php',
|
|
||||||
'Sabberworm\\CSS\\CSSList\\CSSList' => $vendorDir . '/sabberworm/php-css-parser/src/CSSList/CSSList.php',
|
|
||||||
'Sabberworm\\CSS\\CSSList\\Document' => $vendorDir . '/sabberworm/php-css-parser/src/CSSList/Document.php',
|
|
||||||
'Sabberworm\\CSS\\CSSList\\KeyFrame' => $vendorDir . '/sabberworm/php-css-parser/src/CSSList/KeyFrame.php',
|
|
||||||
'Sabberworm\\CSS\\Comment\\Comment' => $vendorDir . '/sabberworm/php-css-parser/src/Comment/Comment.php',
|
|
||||||
'Sabberworm\\CSS\\Comment\\Commentable' => $vendorDir . '/sabberworm/php-css-parser/src/Comment/Commentable.php',
|
|
||||||
'Sabberworm\\CSS\\OutputFormat' => $vendorDir . '/sabberworm/php-css-parser/src/OutputFormat.php',
|
|
||||||
'Sabberworm\\CSS\\OutputFormatter' => $vendorDir . '/sabberworm/php-css-parser/src/OutputFormatter.php',
|
|
||||||
'Sabberworm\\CSS\\Parser' => $vendorDir . '/sabberworm/php-css-parser/src/Parser.php',
|
|
||||||
'Sabberworm\\CSS\\Parsing\\Anchor' => $vendorDir . '/sabberworm/php-css-parser/src/Parsing/Anchor.php',
|
|
||||||
'Sabberworm\\CSS\\Parsing\\OutputException' => $vendorDir . '/sabberworm/php-css-parser/src/Parsing/OutputException.php',
|
|
||||||
'Sabberworm\\CSS\\Parsing\\ParserState' => $vendorDir . '/sabberworm/php-css-parser/src/Parsing/ParserState.php',
|
|
||||||
'Sabberworm\\CSS\\Parsing\\SourceException' => $vendorDir . '/sabberworm/php-css-parser/src/Parsing/SourceException.php',
|
|
||||||
'Sabberworm\\CSS\\Parsing\\UnexpectedEOFException' => $vendorDir . '/sabberworm/php-css-parser/src/Parsing/UnexpectedEOFException.php',
|
|
||||||
'Sabberworm\\CSS\\Parsing\\UnexpectedTokenException' => $vendorDir . '/sabberworm/php-css-parser/src/Parsing/UnexpectedTokenException.php',
|
|
||||||
'Sabberworm\\CSS\\Position\\Position' => $vendorDir . '/sabberworm/php-css-parser/src/Position/Position.php',
|
|
||||||
'Sabberworm\\CSS\\Position\\Positionable' => $vendorDir . '/sabberworm/php-css-parser/src/Position/Positionable.php',
|
|
||||||
'Sabberworm\\CSS\\Property\\AtRule' => $vendorDir . '/sabberworm/php-css-parser/src/Property/AtRule.php',
|
|
||||||
'Sabberworm\\CSS\\Property\\CSSNamespace' => $vendorDir . '/sabberworm/php-css-parser/src/Property/CSSNamespace.php',
|
|
||||||
'Sabberworm\\CSS\\Property\\Charset' => $vendorDir . '/sabberworm/php-css-parser/src/Property/Charset.php',
|
|
||||||
'Sabberworm\\CSS\\Property\\Import' => $vendorDir . '/sabberworm/php-css-parser/src/Property/Import.php',
|
|
||||||
'Sabberworm\\CSS\\Property\\KeyframeSelector' => $vendorDir . '/sabberworm/php-css-parser/src/Property/KeyframeSelector.php',
|
|
||||||
'Sabberworm\\CSS\\Property\\Selector' => $vendorDir . '/sabberworm/php-css-parser/src/Property/Selector.php',
|
|
||||||
'Sabberworm\\CSS\\Renderable' => $vendorDir . '/sabberworm/php-css-parser/src/Renderable.php',
|
|
||||||
'Sabberworm\\CSS\\RuleSet\\AtRuleSet' => $vendorDir . '/sabberworm/php-css-parser/src/RuleSet/AtRuleSet.php',
|
|
||||||
'Sabberworm\\CSS\\RuleSet\\DeclarationBlock' => $vendorDir . '/sabberworm/php-css-parser/src/RuleSet/DeclarationBlock.php',
|
|
||||||
'Sabberworm\\CSS\\RuleSet\\RuleSet' => $vendorDir . '/sabberworm/php-css-parser/src/RuleSet/RuleSet.php',
|
|
||||||
'Sabberworm\\CSS\\Rule\\Rule' => $vendorDir . '/sabberworm/php-css-parser/src/Rule/Rule.php',
|
|
||||||
'Sabberworm\\CSS\\Settings' => $vendorDir . '/sabberworm/php-css-parser/src/Settings.php',
|
|
||||||
'Sabberworm\\CSS\\Value\\CSSFunction' => $vendorDir . '/sabberworm/php-css-parser/src/Value/CSSFunction.php',
|
|
||||||
'Sabberworm\\CSS\\Value\\CSSString' => $vendorDir . '/sabberworm/php-css-parser/src/Value/CSSString.php',
|
|
||||||
'Sabberworm\\CSS\\Value\\CalcFunction' => $vendorDir . '/sabberworm/php-css-parser/src/Value/CalcFunction.php',
|
|
||||||
'Sabberworm\\CSS\\Value\\CalcRuleValueList' => $vendorDir . '/sabberworm/php-css-parser/src/Value/CalcRuleValueList.php',
|
|
||||||
'Sabberworm\\CSS\\Value\\Color' => $vendorDir . '/sabberworm/php-css-parser/src/Value/Color.php',
|
|
||||||
'Sabberworm\\CSS\\Value\\LineName' => $vendorDir . '/sabberworm/php-css-parser/src/Value/LineName.php',
|
|
||||||
'Sabberworm\\CSS\\Value\\PrimitiveValue' => $vendorDir . '/sabberworm/php-css-parser/src/Value/PrimitiveValue.php',
|
|
||||||
'Sabberworm\\CSS\\Value\\RuleValueList' => $vendorDir . '/sabberworm/php-css-parser/src/Value/RuleValueList.php',
|
|
||||||
'Sabberworm\\CSS\\Value\\Size' => $vendorDir . '/sabberworm/php-css-parser/src/Value/Size.php',
|
|
||||||
'Sabberworm\\CSS\\Value\\URL' => $vendorDir . '/sabberworm/php-css-parser/src/Value/URL.php',
|
|
||||||
'Sabberworm\\CSS\\Value\\Value' => $vendorDir . '/sabberworm/php-css-parser/src/Value/Value.php',
|
|
||||||
'Sabberworm\\CSS\\Value\\ValueList' => $vendorDir . '/sabberworm/php-css-parser/src/Value/ValueList.php',
|
|
||||||
'Svg\\CssLength' => $vendorDir . '/phenx/php-svg-lib/src/Svg/CssLength.php',
|
|
||||||
'Svg\\DefaultStyle' => $vendorDir . '/phenx/php-svg-lib/src/Svg/DefaultStyle.php',
|
|
||||||
'Svg\\Document' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Document.php',
|
|
||||||
'Svg\\Gradient\\Stop' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Gradient/Stop.php',
|
|
||||||
'Svg\\Style' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Style.php',
|
|
||||||
'Svg\\Surface\\CPdf' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Surface/CPdf.php',
|
|
||||||
'Svg\\Surface\\SurfaceCpdf' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Surface/SurfaceCpdf.php',
|
|
||||||
'Svg\\Surface\\SurfaceInterface' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Surface/SurfaceInterface.php',
|
|
||||||
'Svg\\Surface\\SurfacePDFLib' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Surface/SurfacePDFLib.php',
|
|
||||||
'Svg\\Tag\\AbstractTag' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Tag/AbstractTag.php',
|
|
||||||
'Svg\\Tag\\Anchor' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Tag/Anchor.php',
|
|
||||||
'Svg\\Tag\\Circle' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Tag/Circle.php',
|
|
||||||
'Svg\\Tag\\ClipPath' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Tag/ClipPath.php',
|
|
||||||
'Svg\\Tag\\Ellipse' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Tag/Ellipse.php',
|
|
||||||
'Svg\\Tag\\Group' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Tag/Group.php',
|
|
||||||
'Svg\\Tag\\Image' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Tag/Image.php',
|
|
||||||
'Svg\\Tag\\Line' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Tag/Line.php',
|
|
||||||
'Svg\\Tag\\LinearGradient' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Tag/LinearGradient.php',
|
|
||||||
'Svg\\Tag\\Path' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Tag/Path.php',
|
|
||||||
'Svg\\Tag\\Polygon' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Tag/Polygon.php',
|
|
||||||
'Svg\\Tag\\Polyline' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Tag/Polyline.php',
|
|
||||||
'Svg\\Tag\\RadialGradient' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Tag/RadialGradient.php',
|
|
||||||
'Svg\\Tag\\Rect' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Tag/Rect.php',
|
|
||||||
'Svg\\Tag\\Shape' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Tag/Shape.php',
|
|
||||||
'Svg\\Tag\\Stop' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Tag/Stop.php',
|
|
||||||
'Svg\\Tag\\StyleTag' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Tag/StyleTag.php',
|
|
||||||
'Svg\\Tag\\Symbol' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Tag/Symbol.php',
|
|
||||||
'Svg\\Tag\\Text' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Tag/Text.php',
|
|
||||||
'Svg\\Tag\\UseTag' => $vendorDir . '/phenx/php-svg-lib/src/Svg/Tag/UseTag.php',
|
|
||||||
);
|
|
||||||
9
pirp/vendor/composer/autoload_namespaces.php
vendored
9
pirp/vendor/composer/autoload_namespaces.php
vendored
@@ -1,9 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
// autoload_namespaces.php @generated by Composer
|
|
||||||
|
|
||||||
$vendorDir = dirname(__DIR__);
|
|
||||||
$baseDir = dirname($vendorDir);
|
|
||||||
|
|
||||||
return array(
|
|
||||||
);
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user