Added support for Streamdeck Pedal and updated UI to better fit the Packed UI style
This commit is contained in:
9
pirp/.claude/settings.local.json
Normal file
9
pirp/.claude/settings.local.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(done)",
|
||||
"Bash(python3:*)",
|
||||
"Bash(grep:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
4
pirp/.dockerignore
Normal file
4
pirp/.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
vendor/
|
||||
public/uploads/expenses/
|
||||
public/uploads/invoices/
|
||||
.git/
|
||||
118
pirp/CLAUDE.md
Normal file
118
pirp/CLAUDE.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# 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
|
||||
13
pirp/Dockerfile
Normal file
13
pirp/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
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"]
|
||||
13
pirp/composer.json
Normal file
13
pirp/composer.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"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
Normal file
304
pirp/composer.lock
generated
Normal file
@@ -0,0 +1,304 @@
|
||||
{
|
||||
"_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
Executable file
31
pirp/dev.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/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
|
||||
36
pirp/docker-compose.yml
Normal file
36
pirp/docker-compose.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
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:
|
||||
184
pirp/public/assets/combobox.js
Normal file
184
pirp/public/assets/combobox.js
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
202
pirp/public/assets/command-palette.js
Normal file
202
pirp/public/assets/command-palette.js
Normal file
@@ -0,0 +1,202 @@
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
})();
|
||||
1391
pirp/public/assets/style.css
Normal file
1391
pirp/public/assets/style.css
Normal file
File diff suppressed because it is too large
Load Diff
192
pirp/public/belegarchiv.php
Normal file
192
pirp/public/belegarchiv.php
Normal file
@@ -0,0 +1,192 @@
|
||||
<?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>
|
||||
139
pirp/public/customers.php
Normal file
139
pirp/public/customers.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?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>
|
||||
336
pirp/public/euer.php
Normal file
336
pirp/public/euer.php
Normal file
@@ -0,0 +1,336 @@
|
||||
<?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>
|
||||
10
pirp/public/euer_export.php
Normal file
10
pirp/public/euer_export.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?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;
|
||||
29
pirp/public/expense_file.php
Normal file
29
pirp/public/expense_file.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?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;
|
||||
|
||||
389
pirp/public/expenses.php
Normal file
389
pirp/public/expenses.php
Normal file
@@ -0,0 +1,389 @@
|
||||
<?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>
|
||||
422
pirp/public/index.php
Normal file
422
pirp/public/index.php
Normal file
@@ -0,0 +1,422 @@
|
||||
<?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>
|
||||
224
pirp/public/invoice_new.php
Normal file
224
pirp/public/invoice_new.php
Normal file
@@ -0,0 +1,224 @@
|
||||
<?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>
|
||||
85
pirp/public/invoice_pdf.php
Normal file
85
pirp/public/invoice_pdf.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?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;
|
||||
129
pirp/public/invoice_storno.php
Normal file
129
pirp/public/invoice_storno.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?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>
|
||||
248
pirp/public/invoices.php
Normal file
248
pirp/public/invoices.php
Normal file
@@ -0,0 +1,248 @@
|
||||
<?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>
|
||||
60
pirp/public/invoices_csv.php
Normal file
60
pirp/public/invoices_csv.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?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;
|
||||
559
pirp/public/journal.php
Normal file
559
pirp/public/journal.php
Normal file
@@ -0,0 +1,559 @@
|
||||
<?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>
|
||||
81
pirp/public/journal_api.php
Normal file
81
pirp/public/journal_api.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?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()]);
|
||||
}
|
||||
168
pirp/public/journal_detail.php
Normal file
168
pirp/public/journal_detail.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?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>
|
||||
311
pirp/public/journal_entry.php
Normal file
311
pirp/public/journal_entry.php
Normal file
@@ -0,0 +1,311 @@
|
||||
<?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>
|
||||
20
pirp/public/journal_euer.php
Normal file
20
pirp/public/journal_euer.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?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;
|
||||
159
pirp/public/journal_search.php
Normal file
159
pirp/public/journal_search.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?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>
|
||||
5
pirp/public/journal_settings.php
Normal file
5
pirp/public/journal_settings.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
// Redirect to main settings with journal tab
|
||||
require_once __DIR__ . '/../src/config.php';
|
||||
header('Location: ' . url_for('settings.php?tab=journal'));
|
||||
exit;
|
||||
248
pirp/public/journal_summary.php
Normal file
248
pirp/public/journal_summary.php
Normal file
@@ -0,0 +1,248 @@
|
||||
<?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>
|
||||
42
pirp/public/login.php
Normal file
42
pirp/public/login.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?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>
|
||||
6
pirp/public/logout.php
Normal file
6
pirp/public/logout.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../src/config.php';
|
||||
require_once __DIR__ . '/../src/auth.php';
|
||||
logout();
|
||||
header('Location: ' . url_for('login.php'));
|
||||
exit;
|
||||
128
pirp/public/mahnung_new.php
Normal file
128
pirp/public/mahnung_new.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?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>
|
||||
45
pirp/public/mahnung_pdf.php
Normal file
45
pirp/public/mahnung_pdf.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?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;
|
||||
147
pirp/public/recurring.php
Normal file
147
pirp/public/recurring.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?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>
|
||||
277
pirp/public/recurring_edit.php
Normal file
277
pirp/public/recurring_edit.php
Normal file
@@ -0,0 +1,277 @@
|
||||
<?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>
|
||||
158
pirp/public/recurring_generate.php
Normal file
158
pirp/public/recurring_generate.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?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>
|
||||
81
pirp/public/search_api.php
Normal file
81
pirp/public/search_api.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?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]);
|
||||
802
pirp/public/settings.php
Normal file
802
pirp/public/settings.php
Normal file
@@ -0,0 +1,802 @@
|
||||
<?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>
|
||||
BIN
pirp/public/uploads/logo.png
Normal file
BIN
pirp/public/uploads/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 469 KiB |
113
pirp/reset.sh
Normal file
113
pirp/reset.sh
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/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
Normal file
270
pirp/schema.sql
Normal file
@@ -0,0 +1,270 @@
|
||||
-- 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);
|
||||
70
pirp/src/auth.php
Normal file
70
pirp/src/auth.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?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;
|
||||
}
|
||||
29
pirp/src/config.php
Normal file
29
pirp/src/config.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?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;
|
||||
}
|
||||
61
pirp/src/customer_functions.php
Normal file
61
pirp/src/customer_functions.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?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]);
|
||||
}
|
||||
14
pirp/src/db.php
Normal file
14
pirp/src/db.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?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;
|
||||
}
|
||||
117
pirp/src/expense_functions.php
Normal file
117
pirp/src/expense_functions.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?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]);
|
||||
}
|
||||
|
||||
39
pirp/src/icons.php
Normal file
39
pirp/src/icons.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?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>';
|
||||
}
|
||||
245
pirp/src/invoice_functions.php
Normal file
245
pirp/src/invoice_functions.php
Normal file
@@ -0,0 +1,245 @@
|
||||
<?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;
|
||||
}
|
||||
1521
pirp/src/journal_functions.php
Normal file
1521
pirp/src/journal_functions.php
Normal file
File diff suppressed because it is too large
Load Diff
635
pirp/src/pdf_functions.php
Normal file
635
pirp/src/pdf_functions.php
Normal file
@@ -0,0 +1,635 @@
|
||||
<?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;
|
||||
}
|
||||
327
pirp/src/recurring_functions.php
Normal file
327
pirp/src/recurring_functions.php
Normal file
@@ -0,0 +1,327 @@
|
||||
<?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
|
||||
};
|
||||
}
|
||||
13
pirp/tools/hash.php
Normal file
13
pirp/tools/hash.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?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";
|
||||
149
pirp/tools/import_excel_categories.php
Normal file
149
pirp/tools/import_excel_categories.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?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";
|
||||
44
pirp/tools/migrate_deductions.php
Normal file
44
pirp/tools/migrate_deductions.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?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);
|
||||
}
|
||||
72
pirp/tools/migrate_full_update.sql
Normal file
72
pirp/tools/migrate_full_update.sql
Normal file
@@ -0,0 +1,72 @@
|
||||
-- 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;
|
||||
98
pirp/tools/migrate_journal.sql
Normal file
98
pirp/tools/migrate_journal.sql
Normal file
@@ -0,0 +1,98 @@
|
||||
-- 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);
|
||||
30
pirp/tools/migrate_journal_auto.sql
Normal file
30
pirp/tools/migrate_journal_auto.sql
Normal file
@@ -0,0 +1,30 @@
|
||||
-- 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';
|
||||
96
pirp/tools/migrate_journal_entries.php
Normal file
96
pirp/tools/migrate_journal_entries.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?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";
|
||||
}
|
||||
36
pirp/tools/migrate_journal_invoice_link.php
Normal file
36
pirp/tools/migrate_journal_invoice_link.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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);
|
||||
}
|
||||
5
pirp/tools/migrate_journal_invoice_link.sql
Normal file
5
pirp/tools/migrate_journal_invoice_link.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- 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);
|
||||
15
pirp/tools/migrate_pdf.sql
Normal file
15
pirp/tools/migrate_pdf.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
-- 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;
|
||||
53
pirp/tools/migrate_pdfs.php
Normal file
53
pirp/tools/migrate_pdfs.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?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";
|
||||
42
pirp/tools/migrate_recurring.sql
Normal file
42
pirp/tools/migrate_recurring.sql
Normal file
@@ -0,0 +1,42 @@
|
||||
-- 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);
|
||||
20
pirp/tools/migrate_storno_mahnung.sql
Normal file
20
pirp/tools/migrate_storno_mahnung.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
-- 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);
|
||||
19
pirp/tools/migrate_unique_constraints.sql
Normal file
19
pirp/tools/migrate_unique_constraints.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- 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;
|
||||
75
pirp/tools/run_migration.php
Normal file
75
pirp/tools/run_migration.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?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";
|
||||
4
pirp/tools/seed_dev.sql
Normal file
4
pirp/tools/seed_dev.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- 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
Normal file
25
pirp/vendor/autoload.php
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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
Normal file
579
pirp/vendor/composer/ClassLoader.php
vendored
Normal file
@@ -0,0 +1,579 @@
|
||||
<?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
Normal file
359
pirp/vendor/composer/InstalledVersions.php
vendored
Normal file
@@ -0,0 +1,359 @@
|
||||
<?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
Normal file
19
pirp/vendor/composer/LICENSE
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
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
Normal file
209
pirp/vendor/composer/autoload_classmap.php
vendored
Normal file
@@ -0,0 +1,209 @@
|
||||
<?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
Normal file
9
pirp/vendor/composer/autoload_namespaces.php
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
// autoload_namespaces.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
);
|
||||
15
pirp/vendor/composer/autoload_psr4.php
vendored
Normal file
15
pirp/vendor/composer/autoload_psr4.php
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
// autoload_psr4.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Svg\\' => array($vendorDir . '/phenx/php-svg-lib/src/Svg'),
|
||||
'Sabberworm\\CSS\\' => array($vendorDir . '/sabberworm/php-css-parser/src'),
|
||||
'Masterminds\\' => array($vendorDir . '/masterminds/html5/src'),
|
||||
'FontLib\\' => array($vendorDir . '/phenx/php-font-lib/src/FontLib'),
|
||||
'Dompdf\\' => array($vendorDir . '/dompdf/dompdf/src'),
|
||||
'' => array($baseDir . '/src'),
|
||||
);
|
||||
38
pirp/vendor/composer/autoload_real.php
vendored
Normal file
38
pirp/vendor/composer/autoload_real.php
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
// autoload_real.php @generated by Composer
|
||||
|
||||
class ComposerAutoloaderInit76bd9531733da7ca24f4b785b8fe430d
|
||||
{
|
||||
private static $loader;
|
||||
|
||||
public static function loadClassLoader($class)
|
||||
{
|
||||
if ('Composer\Autoload\ClassLoader' === $class) {
|
||||
require __DIR__ . '/ClassLoader.php';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Composer\Autoload\ClassLoader
|
||||
*/
|
||||
public static function getLoader()
|
||||
{
|
||||
if (null !== self::$loader) {
|
||||
return self::$loader;
|
||||
}
|
||||
|
||||
require __DIR__ . '/platform_check.php';
|
||||
|
||||
spl_autoload_register(array('ComposerAutoloaderInit76bd9531733da7ca24f4b785b8fe430d', 'loadClassLoader'), true, true);
|
||||
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
|
||||
spl_autoload_unregister(array('ComposerAutoloaderInit76bd9531733da7ca24f4b785b8fe430d', 'loadClassLoader'));
|
||||
|
||||
require __DIR__ . '/autoload_static.php';
|
||||
call_user_func(\Composer\Autoload\ComposerStaticInit76bd9531733da7ca24f4b785b8fe430d::getInitializer($loader));
|
||||
|
||||
$loader->register(true);
|
||||
|
||||
return $loader;
|
||||
}
|
||||
}
|
||||
269
pirp/vendor/composer/autoload_static.php
vendored
Normal file
269
pirp/vendor/composer/autoload_static.php
vendored
Normal file
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
|
||||
// autoload_static.php @generated by Composer
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
class ComposerStaticInit76bd9531733da7ca24f4b785b8fe430d
|
||||
{
|
||||
public static $prefixLengthsPsr4 = array (
|
||||
'S' =>
|
||||
array (
|
||||
'Svg\\' => 4,
|
||||
'Sabberworm\\CSS\\' => 15,
|
||||
),
|
||||
'M' =>
|
||||
array (
|
||||
'Masterminds\\' => 12,
|
||||
),
|
||||
'F' =>
|
||||
array (
|
||||
'FontLib\\' => 8,
|
||||
),
|
||||
'D' =>
|
||||
array (
|
||||
'Dompdf\\' => 7,
|
||||
),
|
||||
);
|
||||
|
||||
public static $prefixDirsPsr4 = array (
|
||||
'Svg\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg',
|
||||
),
|
||||
'Sabberworm\\CSS\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/sabberworm/php-css-parser/src',
|
||||
),
|
||||
'Masterminds\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/masterminds/html5/src',
|
||||
),
|
||||
'FontLib\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib',
|
||||
),
|
||||
'Dompdf\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/dompdf/dompdf/src',
|
||||
),
|
||||
);
|
||||
|
||||
public static $fallbackDirsPsr4 = array (
|
||||
0 => __DIR__ . '/../..' . '/src',
|
||||
);
|
||||
|
||||
public static $classMap = array (
|
||||
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
|
||||
'Dompdf\\Adapter\\CPDF' => __DIR__ . '/..' . '/dompdf/dompdf/src/Adapter/CPDF.php',
|
||||
'Dompdf\\Adapter\\GD' => __DIR__ . '/..' . '/dompdf/dompdf/src/Adapter/GD.php',
|
||||
'Dompdf\\Adapter\\PDFLib' => __DIR__ . '/..' . '/dompdf/dompdf/src/Adapter/PDFLib.php',
|
||||
'Dompdf\\Canvas' => __DIR__ . '/..' . '/dompdf/dompdf/src/Canvas.php',
|
||||
'Dompdf\\CanvasFactory' => __DIR__ . '/..' . '/dompdf/dompdf/src/CanvasFactory.php',
|
||||
'Dompdf\\Cellmap' => __DIR__ . '/..' . '/dompdf/dompdf/src/Cellmap.php',
|
||||
'Dompdf\\Cpdf' => __DIR__ . '/..' . '/dompdf/dompdf/lib/Cpdf.php',
|
||||
'Dompdf\\Css\\AttributeTranslator' => __DIR__ . '/..' . '/dompdf/dompdf/src/Css/AttributeTranslator.php',
|
||||
'Dompdf\\Css\\Color' => __DIR__ . '/..' . '/dompdf/dompdf/src/Css/Color.php',
|
||||
'Dompdf\\Css\\Style' => __DIR__ . '/..' . '/dompdf/dompdf/src/Css/Style.php',
|
||||
'Dompdf\\Css\\Stylesheet' => __DIR__ . '/..' . '/dompdf/dompdf/src/Css/Stylesheet.php',
|
||||
'Dompdf\\Dompdf' => __DIR__ . '/..' . '/dompdf/dompdf/src/Dompdf.php',
|
||||
'Dompdf\\Exception' => __DIR__ . '/..' . '/dompdf/dompdf/src/Exception.php',
|
||||
'Dompdf\\Exception\\ImageException' => __DIR__ . '/..' . '/dompdf/dompdf/src/Exception/ImageException.php',
|
||||
'Dompdf\\FontMetrics' => __DIR__ . '/..' . '/dompdf/dompdf/src/FontMetrics.php',
|
||||
'Dompdf\\Frame' => __DIR__ . '/..' . '/dompdf/dompdf/src/Frame.php',
|
||||
'Dompdf\\FrameDecorator\\AbstractFrameDecorator' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameDecorator/AbstractFrameDecorator.php',
|
||||
'Dompdf\\FrameDecorator\\Block' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameDecorator/Block.php',
|
||||
'Dompdf\\FrameDecorator\\Image' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameDecorator/Image.php',
|
||||
'Dompdf\\FrameDecorator\\Inline' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameDecorator/Inline.php',
|
||||
'Dompdf\\FrameDecorator\\ListBullet' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameDecorator/ListBullet.php',
|
||||
'Dompdf\\FrameDecorator\\ListBulletImage' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameDecorator/ListBulletImage.php',
|
||||
'Dompdf\\FrameDecorator\\NullFrameDecorator' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameDecorator/NullFrameDecorator.php',
|
||||
'Dompdf\\FrameDecorator\\Page' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameDecorator/Page.php',
|
||||
'Dompdf\\FrameDecorator\\Table' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameDecorator/Table.php',
|
||||
'Dompdf\\FrameDecorator\\TableCell' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameDecorator/TableCell.php',
|
||||
'Dompdf\\FrameDecorator\\TableRow' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameDecorator/TableRow.php',
|
||||
'Dompdf\\FrameDecorator\\TableRowGroup' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameDecorator/TableRowGroup.php',
|
||||
'Dompdf\\FrameDecorator\\Text' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameDecorator/Text.php',
|
||||
'Dompdf\\FrameReflower\\AbstractFrameReflower' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameReflower/AbstractFrameReflower.php',
|
||||
'Dompdf\\FrameReflower\\Block' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameReflower/Block.php',
|
||||
'Dompdf\\FrameReflower\\Image' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameReflower/Image.php',
|
||||
'Dompdf\\FrameReflower\\Inline' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameReflower/Inline.php',
|
||||
'Dompdf\\FrameReflower\\ListBullet' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameReflower/ListBullet.php',
|
||||
'Dompdf\\FrameReflower\\NullFrameReflower' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameReflower/NullFrameReflower.php',
|
||||
'Dompdf\\FrameReflower\\Page' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameReflower/Page.php',
|
||||
'Dompdf\\FrameReflower\\Table' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameReflower/Table.php',
|
||||
'Dompdf\\FrameReflower\\TableCell' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameReflower/TableCell.php',
|
||||
'Dompdf\\FrameReflower\\TableRow' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameReflower/TableRow.php',
|
||||
'Dompdf\\FrameReflower\\TableRowGroup' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameReflower/TableRowGroup.php',
|
||||
'Dompdf\\FrameReflower\\Text' => __DIR__ . '/..' . '/dompdf/dompdf/src/FrameReflower/Text.php',
|
||||
'Dompdf\\Frame\\Factory' => __DIR__ . '/..' . '/dompdf/dompdf/src/Frame/Factory.php',
|
||||
'Dompdf\\Frame\\FrameListIterator' => __DIR__ . '/..' . '/dompdf/dompdf/src/Frame/FrameListIterator.php',
|
||||
'Dompdf\\Frame\\FrameTree' => __DIR__ . '/..' . '/dompdf/dompdf/src/Frame/FrameTree.php',
|
||||
'Dompdf\\Frame\\FrameTreeIterator' => __DIR__ . '/..' . '/dompdf/dompdf/src/Frame/FrameTreeIterator.php',
|
||||
'Dompdf\\Helpers' => __DIR__ . '/..' . '/dompdf/dompdf/src/Helpers.php',
|
||||
'Dompdf\\Image\\Cache' => __DIR__ . '/..' . '/dompdf/dompdf/src/Image/Cache.php',
|
||||
'Dompdf\\JavascriptEmbedder' => __DIR__ . '/..' . '/dompdf/dompdf/src/JavascriptEmbedder.php',
|
||||
'Dompdf\\LineBox' => __DIR__ . '/..' . '/dompdf/dompdf/src/LineBox.php',
|
||||
'Dompdf\\Options' => __DIR__ . '/..' . '/dompdf/dompdf/src/Options.php',
|
||||
'Dompdf\\PhpEvaluator' => __DIR__ . '/..' . '/dompdf/dompdf/src/PhpEvaluator.php',
|
||||
'Dompdf\\Positioner\\Absolute' => __DIR__ . '/..' . '/dompdf/dompdf/src/Positioner/Absolute.php',
|
||||
'Dompdf\\Positioner\\AbstractPositioner' => __DIR__ . '/..' . '/dompdf/dompdf/src/Positioner/AbstractPositioner.php',
|
||||
'Dompdf\\Positioner\\Block' => __DIR__ . '/..' . '/dompdf/dompdf/src/Positioner/Block.php',
|
||||
'Dompdf\\Positioner\\Fixed' => __DIR__ . '/..' . '/dompdf/dompdf/src/Positioner/Fixed.php',
|
||||
'Dompdf\\Positioner\\Inline' => __DIR__ . '/..' . '/dompdf/dompdf/src/Positioner/Inline.php',
|
||||
'Dompdf\\Positioner\\ListBullet' => __DIR__ . '/..' . '/dompdf/dompdf/src/Positioner/ListBullet.php',
|
||||
'Dompdf\\Positioner\\NullPositioner' => __DIR__ . '/..' . '/dompdf/dompdf/src/Positioner/NullPositioner.php',
|
||||
'Dompdf\\Positioner\\TableCell' => __DIR__ . '/..' . '/dompdf/dompdf/src/Positioner/TableCell.php',
|
||||
'Dompdf\\Positioner\\TableRow' => __DIR__ . '/..' . '/dompdf/dompdf/src/Positioner/TableRow.php',
|
||||
'Dompdf\\Renderer' => __DIR__ . '/..' . '/dompdf/dompdf/src/Renderer.php',
|
||||
'Dompdf\\Renderer\\AbstractRenderer' => __DIR__ . '/..' . '/dompdf/dompdf/src/Renderer/AbstractRenderer.php',
|
||||
'Dompdf\\Renderer\\Block' => __DIR__ . '/..' . '/dompdf/dompdf/src/Renderer/Block.php',
|
||||
'Dompdf\\Renderer\\Image' => __DIR__ . '/..' . '/dompdf/dompdf/src/Renderer/Image.php',
|
||||
'Dompdf\\Renderer\\Inline' => __DIR__ . '/..' . '/dompdf/dompdf/src/Renderer/Inline.php',
|
||||
'Dompdf\\Renderer\\ListBullet' => __DIR__ . '/..' . '/dompdf/dompdf/src/Renderer/ListBullet.php',
|
||||
'Dompdf\\Renderer\\TableCell' => __DIR__ . '/..' . '/dompdf/dompdf/src/Renderer/TableCell.php',
|
||||
'Dompdf\\Renderer\\TableRowGroup' => __DIR__ . '/..' . '/dompdf/dompdf/src/Renderer/TableRowGroup.php',
|
||||
'Dompdf\\Renderer\\Text' => __DIR__ . '/..' . '/dompdf/dompdf/src/Renderer/Text.php',
|
||||
'FontLib\\AdobeFontMetrics' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/AdobeFontMetrics.php',
|
||||
'FontLib\\BinaryStream' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/BinaryStream.php',
|
||||
'FontLib\\EOT\\File' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/EOT/File.php',
|
||||
'FontLib\\EOT\\Header' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/EOT/Header.php',
|
||||
'FontLib\\EncodingMap' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/EncodingMap.php',
|
||||
'FontLib\\Exception\\FontNotFoundException' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Exception/FontNotFoundException.php',
|
||||
'FontLib\\Font' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Font.php',
|
||||
'FontLib\\Glyph\\Outline' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Glyph/Outline.php',
|
||||
'FontLib\\Glyph\\OutlineComponent' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Glyph/OutlineComponent.php',
|
||||
'FontLib\\Glyph\\OutlineComposite' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Glyph/OutlineComposite.php',
|
||||
'FontLib\\Glyph\\OutlineSimple' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Glyph/OutlineSimple.php',
|
||||
'FontLib\\Header' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Header.php',
|
||||
'FontLib\\OpenType\\File' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/OpenType/File.php',
|
||||
'FontLib\\OpenType\\TableDirectoryEntry' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/OpenType/TableDirectoryEntry.php',
|
||||
'FontLib\\Table\\DirectoryEntry' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Table/DirectoryEntry.php',
|
||||
'FontLib\\Table\\Table' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Table/Table.php',
|
||||
'FontLib\\Table\\Type\\cmap' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Table/Type/cmap.php',
|
||||
'FontLib\\Table\\Type\\cvt' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Table/Type/cvt.php',
|
||||
'FontLib\\Table\\Type\\fpgm' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Table/Type/fpgm.php',
|
||||
'FontLib\\Table\\Type\\glyf' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Table/Type/glyf.php',
|
||||
'FontLib\\Table\\Type\\head' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Table/Type/head.php',
|
||||
'FontLib\\Table\\Type\\hhea' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Table/Type/hhea.php',
|
||||
'FontLib\\Table\\Type\\hmtx' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Table/Type/hmtx.php',
|
||||
'FontLib\\Table\\Type\\kern' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Table/Type/kern.php',
|
||||
'FontLib\\Table\\Type\\loca' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Table/Type/loca.php',
|
||||
'FontLib\\Table\\Type\\maxp' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Table/Type/maxp.php',
|
||||
'FontLib\\Table\\Type\\name' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Table/Type/name.php',
|
||||
'FontLib\\Table\\Type\\nameRecord' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Table/Type/nameRecord.php',
|
||||
'FontLib\\Table\\Type\\os2' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Table/Type/os2.php',
|
||||
'FontLib\\Table\\Type\\post' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Table/Type/post.php',
|
||||
'FontLib\\Table\\Type\\prep' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/Table/Type/prep.php',
|
||||
'FontLib\\TrueType\\Collection' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/TrueType/Collection.php',
|
||||
'FontLib\\TrueType\\File' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/TrueType/File.php',
|
||||
'FontLib\\TrueType\\Header' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/TrueType/Header.php',
|
||||
'FontLib\\TrueType\\TableDirectoryEntry' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/TrueType/TableDirectoryEntry.php',
|
||||
'FontLib\\WOFF\\File' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/WOFF/File.php',
|
||||
'FontLib\\WOFF\\Header' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/WOFF/Header.php',
|
||||
'FontLib\\WOFF\\TableDirectoryEntry' => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib/WOFF/TableDirectoryEntry.php',
|
||||
'Masterminds\\HTML5' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5.php',
|
||||
'Masterminds\\HTML5\\Elements' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Elements.php',
|
||||
'Masterminds\\HTML5\\Entities' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Entities.php',
|
||||
'Masterminds\\HTML5\\Exception' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Exception.php',
|
||||
'Masterminds\\HTML5\\InstructionProcessor' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/InstructionProcessor.php',
|
||||
'Masterminds\\HTML5\\Parser\\CharacterReference' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/CharacterReference.php',
|
||||
'Masterminds\\HTML5\\Parser\\DOMTreeBuilder' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/DOMTreeBuilder.php',
|
||||
'Masterminds\\HTML5\\Parser\\EventHandler' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/EventHandler.php',
|
||||
'Masterminds\\HTML5\\Parser\\FileInputStream' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/FileInputStream.php',
|
||||
'Masterminds\\HTML5\\Parser\\InputStream' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/InputStream.php',
|
||||
'Masterminds\\HTML5\\Parser\\ParseError' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/ParseError.php',
|
||||
'Masterminds\\HTML5\\Parser\\Scanner' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/Scanner.php',
|
||||
'Masterminds\\HTML5\\Parser\\StringInputStream' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/StringInputStream.php',
|
||||
'Masterminds\\HTML5\\Parser\\Tokenizer' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/Tokenizer.php',
|
||||
'Masterminds\\HTML5\\Parser\\TreeBuildingRules' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/TreeBuildingRules.php',
|
||||
'Masterminds\\HTML5\\Parser\\UTF8Utils' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/UTF8Utils.php',
|
||||
'Masterminds\\HTML5\\Serializer\\HTML5Entities' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Serializer/HTML5Entities.php',
|
||||
'Masterminds\\HTML5\\Serializer\\OutputRules' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Serializer/OutputRules.php',
|
||||
'Masterminds\\HTML5\\Serializer\\RulesInterface' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Serializer/RulesInterface.php',
|
||||
'Masterminds\\HTML5\\Serializer\\Traverser' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Serializer/Traverser.php',
|
||||
'Sabberworm\\CSS\\CSSElement' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/CSSElement.php',
|
||||
'Sabberworm\\CSS\\CSSList\\AtRuleBlockList' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/CSSList/AtRuleBlockList.php',
|
||||
'Sabberworm\\CSS\\CSSList\\CSSBlockList' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/CSSList/CSSBlockList.php',
|
||||
'Sabberworm\\CSS\\CSSList\\CSSList' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/CSSList/CSSList.php',
|
||||
'Sabberworm\\CSS\\CSSList\\Document' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/CSSList/Document.php',
|
||||
'Sabberworm\\CSS\\CSSList\\KeyFrame' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/CSSList/KeyFrame.php',
|
||||
'Sabberworm\\CSS\\Comment\\Comment' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Comment/Comment.php',
|
||||
'Sabberworm\\CSS\\Comment\\Commentable' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Comment/Commentable.php',
|
||||
'Sabberworm\\CSS\\OutputFormat' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/OutputFormat.php',
|
||||
'Sabberworm\\CSS\\OutputFormatter' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/OutputFormatter.php',
|
||||
'Sabberworm\\CSS\\Parser' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Parser.php',
|
||||
'Sabberworm\\CSS\\Parsing\\Anchor' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Parsing/Anchor.php',
|
||||
'Sabberworm\\CSS\\Parsing\\OutputException' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Parsing/OutputException.php',
|
||||
'Sabberworm\\CSS\\Parsing\\ParserState' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Parsing/ParserState.php',
|
||||
'Sabberworm\\CSS\\Parsing\\SourceException' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Parsing/SourceException.php',
|
||||
'Sabberworm\\CSS\\Parsing\\UnexpectedEOFException' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Parsing/UnexpectedEOFException.php',
|
||||
'Sabberworm\\CSS\\Parsing\\UnexpectedTokenException' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Parsing/UnexpectedTokenException.php',
|
||||
'Sabberworm\\CSS\\Position\\Position' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Position/Position.php',
|
||||
'Sabberworm\\CSS\\Position\\Positionable' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Position/Positionable.php',
|
||||
'Sabberworm\\CSS\\Property\\AtRule' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Property/AtRule.php',
|
||||
'Sabberworm\\CSS\\Property\\CSSNamespace' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Property/CSSNamespace.php',
|
||||
'Sabberworm\\CSS\\Property\\Charset' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Property/Charset.php',
|
||||
'Sabberworm\\CSS\\Property\\Import' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Property/Import.php',
|
||||
'Sabberworm\\CSS\\Property\\KeyframeSelector' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Property/KeyframeSelector.php',
|
||||
'Sabberworm\\CSS\\Property\\Selector' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Property/Selector.php',
|
||||
'Sabberworm\\CSS\\Renderable' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Renderable.php',
|
||||
'Sabberworm\\CSS\\RuleSet\\AtRuleSet' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/RuleSet/AtRuleSet.php',
|
||||
'Sabberworm\\CSS\\RuleSet\\DeclarationBlock' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/RuleSet/DeclarationBlock.php',
|
||||
'Sabberworm\\CSS\\RuleSet\\RuleSet' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/RuleSet/RuleSet.php',
|
||||
'Sabberworm\\CSS\\Rule\\Rule' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Rule/Rule.php',
|
||||
'Sabberworm\\CSS\\Settings' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Settings.php',
|
||||
'Sabberworm\\CSS\\Value\\CSSFunction' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Value/CSSFunction.php',
|
||||
'Sabberworm\\CSS\\Value\\CSSString' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Value/CSSString.php',
|
||||
'Sabberworm\\CSS\\Value\\CalcFunction' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Value/CalcFunction.php',
|
||||
'Sabberworm\\CSS\\Value\\CalcRuleValueList' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Value/CalcRuleValueList.php',
|
||||
'Sabberworm\\CSS\\Value\\Color' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Value/Color.php',
|
||||
'Sabberworm\\CSS\\Value\\LineName' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Value/LineName.php',
|
||||
'Sabberworm\\CSS\\Value\\PrimitiveValue' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Value/PrimitiveValue.php',
|
||||
'Sabberworm\\CSS\\Value\\RuleValueList' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Value/RuleValueList.php',
|
||||
'Sabberworm\\CSS\\Value\\Size' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Value/Size.php',
|
||||
'Sabberworm\\CSS\\Value\\URL' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Value/URL.php',
|
||||
'Sabberworm\\CSS\\Value\\Value' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Value/Value.php',
|
||||
'Sabberworm\\CSS\\Value\\ValueList' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Value/ValueList.php',
|
||||
'Svg\\CssLength' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/CssLength.php',
|
||||
'Svg\\DefaultStyle' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/DefaultStyle.php',
|
||||
'Svg\\Document' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Document.php',
|
||||
'Svg\\Gradient\\Stop' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Gradient/Stop.php',
|
||||
'Svg\\Style' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Style.php',
|
||||
'Svg\\Surface\\CPdf' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Surface/CPdf.php',
|
||||
'Svg\\Surface\\SurfaceCpdf' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Surface/SurfaceCpdf.php',
|
||||
'Svg\\Surface\\SurfaceInterface' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Surface/SurfaceInterface.php',
|
||||
'Svg\\Surface\\SurfacePDFLib' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Surface/SurfacePDFLib.php',
|
||||
'Svg\\Tag\\AbstractTag' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Tag/AbstractTag.php',
|
||||
'Svg\\Tag\\Anchor' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Tag/Anchor.php',
|
||||
'Svg\\Tag\\Circle' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Tag/Circle.php',
|
||||
'Svg\\Tag\\ClipPath' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Tag/ClipPath.php',
|
||||
'Svg\\Tag\\Ellipse' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Tag/Ellipse.php',
|
||||
'Svg\\Tag\\Group' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Tag/Group.php',
|
||||
'Svg\\Tag\\Image' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Tag/Image.php',
|
||||
'Svg\\Tag\\Line' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Tag/Line.php',
|
||||
'Svg\\Tag\\LinearGradient' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Tag/LinearGradient.php',
|
||||
'Svg\\Tag\\Path' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Tag/Path.php',
|
||||
'Svg\\Tag\\Polygon' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Tag/Polygon.php',
|
||||
'Svg\\Tag\\Polyline' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Tag/Polyline.php',
|
||||
'Svg\\Tag\\RadialGradient' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Tag/RadialGradient.php',
|
||||
'Svg\\Tag\\Rect' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Tag/Rect.php',
|
||||
'Svg\\Tag\\Shape' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Tag/Shape.php',
|
||||
'Svg\\Tag\\Stop' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Tag/Stop.php',
|
||||
'Svg\\Tag\\StyleTag' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Tag/StyleTag.php',
|
||||
'Svg\\Tag\\Symbol' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Tag/Symbol.php',
|
||||
'Svg\\Tag\\Text' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Tag/Text.php',
|
||||
'Svg\\Tag\\UseTag' => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg/Tag/UseTag.php',
|
||||
);
|
||||
|
||||
public static function getInitializer(ClassLoader $loader)
|
||||
{
|
||||
return \Closure::bind(function () use ($loader) {
|
||||
$loader->prefixLengthsPsr4 = ComposerStaticInit76bd9531733da7ca24f4b785b8fe430d::$prefixLengthsPsr4;
|
||||
$loader->prefixDirsPsr4 = ComposerStaticInit76bd9531733da7ca24f4b785b8fe430d::$prefixDirsPsr4;
|
||||
$loader->fallbackDirsPsr4 = ComposerStaticInit76bd9531733da7ca24f4b785b8fe430d::$fallbackDirsPsr4;
|
||||
$loader->classMap = ComposerStaticInit76bd9531733da7ca24f4b785b8fe430d::$classMap;
|
||||
|
||||
}, null, ClassLoader::class);
|
||||
}
|
||||
}
|
||||
306
pirp/vendor/composer/installed.json
vendored
Normal file
306
pirp/vendor/composer/installed.json
vendored
Normal file
@@ -0,0 +1,306 @@
|
||||
{
|
||||
"packages": [
|
||||
{
|
||||
"name": "dompdf/dompdf",
|
||||
"version": "v2.0.8",
|
||||
"version_normalized": "2.0.8.0",
|
||||
"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"
|
||||
},
|
||||
"time": "2024-04-29T13:06:17+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"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"
|
||||
},
|
||||
"install-path": "../dompdf/dompdf"
|
||||
},
|
||||
{
|
||||
"name": "masterminds/html5",
|
||||
"version": "2.10.0",
|
||||
"version_normalized": "2.10.0.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"
|
||||
},
|
||||
"time": "2025-07-25T09:04:22+00:00",
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.7-dev"
|
||||
}
|
||||
},
|
||||
"installation-source": "dist",
|
||||
"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"
|
||||
},
|
||||
"install-path": "../masterminds/html5"
|
||||
},
|
||||
{
|
||||
"name": "phenx/php-font-lib",
|
||||
"version": "0.5.6",
|
||||
"version_normalized": "0.5.6.0",
|
||||
"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"
|
||||
},
|
||||
"time": "2024-01-29T14:45:26+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"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"
|
||||
},
|
||||
"install-path": "../phenx/php-font-lib"
|
||||
},
|
||||
{
|
||||
"name": "phenx/php-svg-lib",
|
||||
"version": "0.5.4",
|
||||
"version_normalized": "0.5.4.0",
|
||||
"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"
|
||||
},
|
||||
"time": "2024-04-08T12:52:34+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"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"
|
||||
},
|
||||
"install-path": "../phenx/php-svg-lib"
|
||||
},
|
||||
{
|
||||
"name": "sabberworm/php-css-parser",
|
||||
"version": "v8.9.0",
|
||||
"version_normalized": "8.9.0.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"
|
||||
},
|
||||
"time": "2025-07-11T13:20:48+00:00",
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "9.0.x-dev"
|
||||
}
|
||||
},
|
||||
"installation-source": "dist",
|
||||
"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"
|
||||
},
|
||||
"install-path": "../sabberworm/php-css-parser"
|
||||
}
|
||||
],
|
||||
"dev": false,
|
||||
"dev-package-names": []
|
||||
}
|
||||
68
pirp/vendor/composer/installed.php
vendored
Normal file
68
pirp/vendor/composer/installed.php
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php return array(
|
||||
'root' => array(
|
||||
'name' => 'pirp/packed-internes-rechnungsprogramm',
|
||||
'pretty_version' => '1.0.0+no-version-set',
|
||||
'version' => '1.0.0.0',
|
||||
'reference' => null,
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev' => false,
|
||||
),
|
||||
'versions' => array(
|
||||
'dompdf/dompdf' => array(
|
||||
'pretty_version' => 'v2.0.8',
|
||||
'version' => '2.0.8.0',
|
||||
'reference' => 'c20247574601700e1f7c8dab39310fca1964dc52',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../dompdf/dompdf',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'masterminds/html5' => array(
|
||||
'pretty_version' => '2.10.0',
|
||||
'version' => '2.10.0.0',
|
||||
'reference' => 'fcf91eb64359852f00d921887b219479b4f21251',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../masterminds/html5',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'phenx/php-font-lib' => array(
|
||||
'pretty_version' => '0.5.6',
|
||||
'version' => '0.5.6.0',
|
||||
'reference' => 'a1681e9793040740a405ac5b189275059e2a9863',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../phenx/php-font-lib',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'phenx/php-svg-lib' => array(
|
||||
'pretty_version' => '0.5.4',
|
||||
'version' => '0.5.4.0',
|
||||
'reference' => '46b25da81613a9cf43c83b2a8c2c1bdab27df691',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../phenx/php-svg-lib',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'pirp/packed-internes-rechnungsprogramm' => array(
|
||||
'pretty_version' => '1.0.0+no-version-set',
|
||||
'version' => '1.0.0.0',
|
||||
'reference' => null,
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'sabberworm/php-css-parser' => array(
|
||||
'pretty_version' => 'v8.9.0',
|
||||
'version' => '8.9.0.0',
|
||||
'reference' => 'd8e916507b88e389e26d4ab03c904a082aa66bb9',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../sabberworm/php-css-parser',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
),
|
||||
);
|
||||
26
pirp/vendor/composer/platform_check.php
vendored
Normal file
26
pirp/vendor/composer/platform_check.php
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
// platform_check.php @generated by Composer
|
||||
|
||||
$issues = array();
|
||||
|
||||
if (!(PHP_VERSION_ID >= 70100)) {
|
||||
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.1.0". You are running ' . PHP_VERSION . '.';
|
||||
}
|
||||
|
||||
if ($issues) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
|
||||
} elseif (!headers_sent()) {
|
||||
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
|
||||
}
|
||||
}
|
||||
trigger_error(
|
||||
'Composer detected issues in your platform: ' . implode(' ', $issues),
|
||||
E_USER_ERROR
|
||||
);
|
||||
}
|
||||
24
pirp/vendor/dompdf/dompdf/AUTHORS.md
vendored
Normal file
24
pirp/vendor/dompdf/dompdf/AUTHORS.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
Dompdf was designed and developed by Benj Carson.
|
||||
|
||||
### Current Team
|
||||
|
||||
* **Brian Sweeney** (maintainer)
|
||||
* **Till Berger**
|
||||
|
||||
### Alumni
|
||||
|
||||
* **Benj Carson** (creator)
|
||||
* **Fabien Ménager**
|
||||
* **Simon Berger**
|
||||
* **Orion Richardson**
|
||||
|
||||
### Contributors
|
||||
* **Gabriel Bull**
|
||||
* **Barry vd. Heuvel**
|
||||
* **Ryan H. Masten**
|
||||
* **Helmut Tischer**
|
||||
* [and many more...](https://github.com/dompdf/dompdf/graphs/contributors)
|
||||
|
||||
### Thanks
|
||||
|
||||
Dompdf would not have been possible without strong community support.
|
||||
456
pirp/vendor/dompdf/dompdf/LICENSE.LGPL
vendored
Normal file
456
pirp/vendor/dompdf/dompdf/LICENSE.LGPL
vendored
Normal file
@@ -0,0 +1,456 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 2.1, February 1999
|
||||
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the Lesser GPL. It also counts
|
||||
as the successor of the GNU Library Public License, version 2, hence
|
||||
the version number 2.1.]
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
Licenses are intended to guarantee your freedom to share and change
|
||||
free software--to make sure the software is free for all its users.
|
||||
|
||||
This license, the Lesser General Public License, applies to some
|
||||
specially designated software packages--typically libraries--of the
|
||||
Free Software Foundation and other authors who decide to use it. You
|
||||
can use it too, but we suggest you first think carefully about whether
|
||||
this license or the ordinary General Public License is the better
|
||||
strategy to use in any particular case, based on the explanations below.
|
||||
|
||||
When we speak of free software, we are referring to freedom of use,
|
||||
not price. Our General Public Licenses are designed to make sure that
|
||||
you have the freedom to distribute copies of free software (and charge
|
||||
for this service if you wish); that you receive source code or can get
|
||||
it if you want it; that you can change the software and use pieces of
|
||||
it in new free programs; and that you are informed that you can do
|
||||
these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
distributors to deny you these rights or to ask you to surrender these
|
||||
rights. These restrictions translate to certain responsibilities for
|
||||
you if you distribute copies of the library or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that we gave
|
||||
you. You must make sure that they, too, receive or can get the source
|
||||
code. If you link other code with the library, you must provide
|
||||
complete object files to the recipients, so that they can relink them
|
||||
with the library after making changes to the library and recompiling
|
||||
it. And you must show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with a two-step method: (1) we copyright the
|
||||
library, and (2) we offer you this license, which gives you legal
|
||||
permission to copy, distribute and/or modify the library.
|
||||
|
||||
To protect each distributor, we want to make it very clear that
|
||||
there is no warranty for the free library. Also, if the library is
|
||||
modified by someone else and passed on, the recipients should know
|
||||
that what they have is not the original version, so that the original
|
||||
author's reputation will not be affected by problems that might be
|
||||
introduced by others.
|
||||
|
||||
Finally, software patents pose a constant threat to the existence of
|
||||
any free program. We wish to make sure that a company cannot
|
||||
effectively restrict the users of a free program by obtaining a
|
||||
restrictive license from a patent holder. Therefore, we insist that
|
||||
any patent license obtained for a version of the library must be
|
||||
consistent with the full freedom of use specified in this license.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the
|
||||
ordinary GNU General Public License. This license, the GNU Lesser
|
||||
General Public License, applies to certain designated libraries, and
|
||||
is quite different from the ordinary General Public License. We use
|
||||
this license for certain libraries in order to permit linking those
|
||||
libraries into non-free programs.
|
||||
|
||||
When a program is linked with a library, whether statically or using
|
||||
a shared library, the combination of the two is legally speaking a
|
||||
combined work, a derivative of the original library. The ordinary
|
||||
General Public License therefore permits such linking only if the
|
||||
entire combination fits its criteria of freedom. The Lesser General
|
||||
Public License permits more lax criteria for linking other code with
|
||||
the library.
|
||||
|
||||
We call this license the "Lesser" General Public License because it
|
||||
does Less to protect the user's freedom than the ordinary General
|
||||
Public License. It also provides other free software developers Less
|
||||
of an advantage over competing non-free programs. These disadvantages
|
||||
are the reason we use the ordinary General Public License for many
|
||||
libraries. However, the Lesser license provides advantages in certain
|
||||
special circumstances.
|
||||
|
||||
For example, on rare occasions, there may be a special need to
|
||||
encourage the widest possible use of a certain library, so that it becomes
|
||||
a de-facto standard. To achieve this, non-free programs must be
|
||||
allowed to use the library. A more frequent case is that a free
|
||||
library does the same job as widely used non-free libraries. In this
|
||||
case, there is little to gain by limiting the free library to free
|
||||
software only, so we use the Lesser General Public License.
|
||||
|
||||
In other cases, permission to use a particular library in non-free
|
||||
programs enables a greater number of people to use a large body of
|
||||
free software. For example, permission to use the GNU C Library in
|
||||
non-free programs enables many more people to use the whole GNU
|
||||
operating system, as well as its variant, the GNU/Linux operating
|
||||
system.
|
||||
|
||||
Although the Lesser General Public License is Less protective of the
|
||||
users' freedom, it does ensure that the user of a program that is
|
||||
linked with the Library has the freedom and the wherewithal to run
|
||||
that program using a modified version of the Library.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, whereas the latter must
|
||||
be combined with the library in order to run.
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library or other
|
||||
program which contains a notice placed by the copyright holder or
|
||||
other authorized party saying it may be distributed under the terms of
|
||||
this Lesser General Public License (also called "this License").
|
||||
Each licensee is addressed as "you".
|
||||
|
||||
A "library" means a collection of software functions and/or data
|
||||
prepared so as to be conveniently linked with application programs
|
||||
(which use some of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work
|
||||
which has been distributed under these terms. A "work based on the
|
||||
Library" means either the Library or any derivative work under
|
||||
copyright law: that is to say, a work containing the Library or a
|
||||
portion of it, either verbatim or with modifications and/or translated
|
||||
straightforwardly into another language. (Hereinafter, translation is
|
||||
included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for
|
||||
making modifications to it. For a library, complete source code means
|
||||
all the source code for all modules it contains, plus any associated
|
||||
interface definition files, plus the scripts used to control compilation
|
||||
and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running a program using the Library is not restricted, and output from
|
||||
such a program is covered only if its contents constitute a work based
|
||||
on the Library (independent of the use of the Library in a tool for
|
||||
writing it). Whether that is true depends on what the Library does
|
||||
and what the program that uses the Library does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Library's
|
||||
complete source code as you receive it, in any medium, provided that
|
||||
you conspicuously and appropriately publish on each copy an
|
||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||
all the notices that refer to this License and to the absence of any
|
||||
warranty; and distribute a copy of this License along with the
|
||||
Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy,
|
||||
and you may at your option offer warranty protection in exchange for a
|
||||
fee.
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion
|
||||
of it, thus forming a work based on the Library, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
b) You must cause the files modified to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
c) You must cause the whole of the work to be licensed at no
|
||||
charge to all third parties under the terms of this License.
|
||||
|
||||
d) If a facility in the modified Library refers to a function or a
|
||||
table of data to be supplied by an application program that uses
|
||||
the facility, other than as an argument passed when the facility
|
||||
is invoked, then you must make a good faith effort to ensure that,
|
||||
in the event an application does not supply such function or
|
||||
table, the facility still operates, and performs whatever part of
|
||||
its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has
|
||||
a purpose that is entirely well-defined independent of the
|
||||
application. Therefore, Subsection 2d requires that any
|
||||
application-supplied function or table used by this function must
|
||||
be optional: if the application does not supply it, the square
|
||||
root function must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Library,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Library, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library
|
||||
with the Library (or with a work based on the Library) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||
License instead of this License to a given copy of the Library. To do
|
||||
this, you must alter all the notices that refer to this License, so
|
||||
that they refer to the ordinary GNU General Public License, version 2,
|
||||
instead of to this License. (If a newer version than version 2 of the
|
||||
ordinary GNU General Public License has appeared, then you can specify
|
||||
that version instead if you wish.) Do not make any other change in
|
||||
these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for
|
||||
that copy, so the ordinary GNU General Public License applies to all
|
||||
subsequent copies and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of
|
||||
the Library into a program that is not a library.
|
||||
|
||||
4. You may copy and distribute the Library (or a portion or
|
||||
derivative of it, under Section 2) in object code or executable form
|
||||
under the terms of Sections 1 and 2 above provided that you accompany
|
||||
it with the complete corresponding machine-readable source code, which
|
||||
must be distributed under the terms of Sections 1 and 2 above on a
|
||||
medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy
|
||||
from a designated place, then offering equivalent access to copy the
|
||||
source code from the same place satisfies the requirement to
|
||||
distribute the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
5. A program that contains no derivative of any portion of the
|
||||
Library, but is designed to work with the Library by being compiled or
|
||||
linked with it, is called a "work that uses the Library". Such a
|
||||
work, in isolation, is not a derivative work of the Library, and
|
||||
therefore falls outside the scope of this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library
|
||||
creates an executable that is a derivative of the Library (because it
|
||||
contains portions of the Library), rather than a "work that uses the
|
||||
library". The executable is therefore covered by this License.
|
||||
Section 6 states terms for distribution of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file
|
||||
that is part of the Library, the object code for the work may be a
|
||||
derivative work of the Library even though the source code is not.
|
||||
Whether this is true is especially significant if the work can be
|
||||
linked without the Library, or if the work is itself a library. The
|
||||
threshold for this to be true is not precisely defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data
|
||||
structure layouts and accessors, and small macros and small inline
|
||||
functions (ten lines or less in length), then the use of the object
|
||||
file is unrestricted, regardless of whether it is legally a derivative
|
||||
work. (Executables containing this object code plus portions of the
|
||||
Library will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may
|
||||
distribute the object code for the work under the terms of Section 6.
|
||||
Any executables containing that work also fall under Section 6,
|
||||
whether or not they are linked directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also combine or
|
||||
link a "work that uses the Library" with the Library to produce a
|
||||
work containing portions of the Library, and distribute that work
|
||||
under terms of your choice, provided that the terms permit
|
||||
modification of the work for the customer's own use and reverse
|
||||
engineering for debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the
|
||||
Library is used in it and that the Library and its use are covered by
|
||||
this License. You must supply a copy of this License. If the work
|
||||
during execution displays copyright notices, you must include the
|
||||
copyright notice for the Library among them, as well as a reference
|
||||
directing the user to the copy of this License. Also, you must do one
|
||||
of these things:
|
||||
|
||||
a) Accompany the work with the complete corresponding
|
||||
machine-readable source code for the Library including whatever
|
||||
changes were used in the work (which must be distributed under
|
||||
Sections 1 and 2 above); and, if the work is an executable linked
|
||||
with the Library, with the complete machine-readable "work that
|
||||
uses the Library", as object code and/or source code, so that the
|
||||
user can modify the Library and then relink to produce a modified
|
||||
executable containing the modified Library. (It is understood
|
||||
that the user who changes the contents of definitions files in the
|
||||
Library will not necessarily be able to recompile the application
|
||||
to use the modified definitions.)
|
||||
|
||||
b) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (1) uses at run time a
|
||||
copy of the library already present on the user's computer system,
|
||||
rather than copying library functions into the executable, and (2)
|
||||
will operate properly with a modified version of the library, if
|
||||
the user installs one, as long as the modified version is
|
||||
interface-compatible with the version that the work was made with.
|
||||
|
||||
c) Accompany the work with a written offer, valid for at
|
||||
least three years, to give the same user the materials
|
||||
specified in Subsection 6a, above, for a charge no more
|
||||
than the cost of performing this distribution.
|
||||
|
||||
d) If distribution of the work is made by offering access to copy
|
||||
from a designated place, offer equivalent access to copy the above
|
||||
specified materials from the same place.
|
||||
|
||||
e) Verify that the user has already received a copy of these
|
||||
materials or that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the
|
||||
Library" must include any data and utility programs needed for
|
||||
reproducing the executable from it. However, as a special exception,
|
||||
the materials to be distributed need not include anything that is
|
||||
normally distributed (in either source or binary form) with the major
|
||||
components (compiler, kernel, and so on) of the operating system on
|
||||
which the executable runs, unless that component itself accompanies
|
||||
the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license
|
||||
restrictions of other proprietary libraries that do not normally
|
||||
accompany the operating system. Such a contradiction means you cannot
|
||||
use both them and the Library together in an executable that you
|
||||
distribute.
|
||||
|
||||
7. You may place library facilities that are a work based on the
|
||||
Library side-by-side in a single library together with other library
|
||||
facilities not covered by this License, and distribute such a combined
|
||||
library, provided that the separate distribution of the work based on
|
||||
the Library and of the other library facilities is otherwise
|
||||
permitted, and provided that you do these two things:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work
|
||||
based on the Library, uncombined with any other library
|
||||
facilities. This must be distributed under the terms of the
|
||||
Sections above.
|
||||
|
||||
b) Give prominent notice with the combined library of the fact
|
||||
that part of it is a work based on the Library, and explaining
|
||||
where to find the accompanying uncombined form of the same work.
|
||||
|
||||
8. You may not copy, modify, sublicense, link with, or distribute
|
||||
the Library except as expressly provided under this License. Any
|
||||
attempt otherwise to copy, modify, sublicense, link with, or
|
||||
distribute the Library is void, and will automatically terminate your
|
||||
rights under this License. However, parties who have received copies,
|
||||
or rights, from you under this License will not have their licenses
|
||||
terminated so long as such parties remain in full compliance.
|
||||
|
||||
9. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Library or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Library (or any work based on the
|
||||
Library), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
10. Each time you redistribute the Library (or any work based on the
|
||||
Library), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute, link with or modify the Library
|
||||
subject to these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties with
|
||||
this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Library at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Library by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply,
|
||||
and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
12. If the distribution and/or use of the Library is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Library under this License may add
|
||||
an explicit geographical distribution limitation excluding those countries,
|
||||
so that distribution is permitted only in or among countries not thus
|
||||
excluded. In such case, this License incorporates the limitation as if
|
||||
written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new
|
||||
versions of the Lesser General Public License from time to time.
|
||||
Such new versions will be similar in spirit to the present version,
|
||||
but may differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library
|
||||
specifies a version number of this License which applies to it and
|
||||
"any later version", you have the option of following the terms and
|
||||
conditions either of that version or of any later version published by
|
||||
the Free Software Foundation. If the Library does not specify a
|
||||
license version number, you may choose any version ever published by
|
||||
the Free Software Foundation.
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free
|
||||
programs whose distribution conditions are incompatible with these,
|
||||
write to the author to ask for permission. For software which is
|
||||
copyrighted by the Free Software Foundation, write to the Free
|
||||
Software Foundation; we sometimes make exceptions for this. Our
|
||||
decision will be guided by the two goals of preserving the free status
|
||||
of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
232
pirp/vendor/dompdf/dompdf/README.md
vendored
Normal file
232
pirp/vendor/dompdf/dompdf/README.md
vendored
Normal file
@@ -0,0 +1,232 @@
|
||||
Dompdf
|
||||
======
|
||||
|
||||
[](https://github.com/dompdf/dompdf/actions/workflows/test.yml)
|
||||
[](https://packagist.org/packages/dompdf/dompdf)
|
||||
[](https://packagist.org/packages/dompdf/dompdf)
|
||||
[](https://packagist.org/packages/dompdf/dompdf)
|
||||
|
||||
**Dompdf is an HTML to PDF converter**
|
||||
|
||||
At its heart, dompdf is (mostly) a [CSS 2.1](http://www.w3.org/TR/CSS2/) compliant
|
||||
HTML layout and rendering engine written in PHP. It is a style-driven renderer:
|
||||
it will download and read external stylesheets, inline style tags, and the style
|
||||
attributes of individual HTML elements. It also supports most presentational
|
||||
HTML attributes.
|
||||
|
||||
*This document applies to the latest stable code which may not reflect the current
|
||||
release. For released code please
|
||||
[navigate to the appropriate tag](https://github.com/dompdf/dompdf/tags).*
|
||||
|
||||
----
|
||||
|
||||
**Check out the [demo](http://eclecticgeek.com/dompdf/debug.php) and ask any
|
||||
question on [StackOverflow](https://stackoverflow.com/questions/tagged/dompdf) or
|
||||
in [Discussions](https://github.com/dompdf/dompdf/discussions).**
|
||||
|
||||
Follow us on [](http://www.twitter.com/dompdf).
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
* Handles most CSS 2.1 and a few CSS3 properties, including @import, @media &
|
||||
@page rules
|
||||
* Supports most presentational HTML 4.0 attributes
|
||||
* Supports external stylesheets, either local or through http/ftp (via
|
||||
fopen-wrappers)
|
||||
* Supports complex tables, including row & column spans, separate & collapsed
|
||||
border models, individual cell styling
|
||||
* Image support (gif, png (8, 24 and 32 bit with alpha channel), bmp & jpeg)
|
||||
* No dependencies on external PDF libraries, thanks to the R&OS PDF class
|
||||
* Inline PHP support
|
||||
* Basic SVG support (see "Limitations" below)
|
||||
|
||||
## Requirements
|
||||
|
||||
* PHP version 7.1 or higher
|
||||
* DOM extension
|
||||
* MBString extension
|
||||
* php-font-lib
|
||||
* php-svg-lib
|
||||
|
||||
Note that some required dependencies may have further dependencies
|
||||
(notably php-svg-lib requires sabberworm/php-css-parser).
|
||||
|
||||
### Recommendations
|
||||
|
||||
* OPcache (OPcache, XCache, APC, etc.): improves performance
|
||||
* GD (for image processing)
|
||||
* IMagick or GMagick extension: improves image processing performance
|
||||
|
||||
Visit the wiki for more information:
|
||||
https://github.com/dompdf/dompdf/wiki/Requirements
|
||||
|
||||
## About Fonts & Character Encoding
|
||||
|
||||
PDF documents internally support the following fonts: Helvetica, Times-Roman,
|
||||
Courier, Zapf-Dingbats, & Symbol. These fonts only support Windows ANSI
|
||||
encoding. In order for a PDF to display characters that are not available in
|
||||
Windows ANSI, you must supply an external font. Dompdf will embed any referenced
|
||||
font in the PDF so long as it has been pre-loaded or is accessible to dompdf and
|
||||
reference in CSS @font-face rules. See the
|
||||
[font overview](https://github.com/dompdf/dompdf/wiki/About-Fonts-and-Character-Encoding)
|
||||
for more information on how to use fonts.
|
||||
|
||||
The [DejaVu TrueType fonts](https://dejavu-fonts.github.io/) have been pre-installed
|
||||
to give dompdf decent Unicode character coverage by default. To use the DejaVu
|
||||
fonts reference the font in your stylesheet, e.g. `body { font-family: DejaVu
|
||||
Sans; }` (for DejaVu Sans). The following DejaVu 2.34 fonts are available:
|
||||
DejaVu Sans, DejaVu Serif, and DejaVu Sans Mono.
|
||||
|
||||
## Easy Installation
|
||||
|
||||
### Install with composer
|
||||
|
||||
To install with [Composer](https://getcomposer.org/), simply require the
|
||||
latest version of this package.
|
||||
|
||||
```bash
|
||||
composer require dompdf/dompdf
|
||||
```
|
||||
|
||||
Make sure that the autoload file from Composer is loaded.
|
||||
|
||||
```php
|
||||
// somewhere early in your project's loading, require the Composer autoloader
|
||||
// see: http://getcomposer.org/doc/00-intro.md
|
||||
require 'vendor/autoload.php';
|
||||
|
||||
```
|
||||
|
||||
### Download and install
|
||||
|
||||
Download a packaged archive of dompdf and extract it into the
|
||||
directory where dompdf will reside
|
||||
|
||||
* You can download stable copies of dompdf from
|
||||
https://github.com/dompdf/dompdf/releases
|
||||
* Or download a nightly (the latest, unreleased code) from
|
||||
http://eclecticgeek.com/dompdf
|
||||
|
||||
Use the packaged release autoloader to load dompdf, libraries,
|
||||
and helper functions in your PHP:
|
||||
|
||||
```php
|
||||
// include autoloader
|
||||
require_once 'dompdf/autoload.inc.php';
|
||||
```
|
||||
|
||||
Note: packaged releases are named according using semantic
|
||||
versioning (_dompdf_MAJOR-MINOR-PATCH.zip_). So the 1.0.0
|
||||
release would be dompdf_1-0-0.zip. This is the only download
|
||||
that includes the autoloader for Dompdf and all its dependencies.
|
||||
|
||||
### Install with git
|
||||
|
||||
From the command line, switch to the directory where dompdf will
|
||||
reside and run the following commands:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/dompdf/dompdf.git
|
||||
cd dompdf/lib
|
||||
|
||||
git clone https://github.com/PhenX/php-font-lib.git php-font-lib
|
||||
cd php-font-lib
|
||||
git checkout 0.5.1
|
||||
cd ..
|
||||
|
||||
git clone https://github.com/PhenX/php-svg-lib.git php-svg-lib
|
||||
cd php-svg-lib
|
||||
git checkout v0.3.2
|
||||
cd ..
|
||||
|
||||
git clone https://github.com/sabberworm/PHP-CSS-Parser.git php-css-parser
|
||||
cd php-css-parser
|
||||
git checkout 8.1.0
|
||||
```
|
||||
|
||||
Require dompdf and it's dependencies in your PHP.
|
||||
For details see the [autoloader in the utils project](https://github.com/dompdf/utils/blob/master/autoload.inc.php).
|
||||
|
||||
## Quick Start
|
||||
|
||||
Just pass your HTML in to dompdf and stream the output:
|
||||
|
||||
```php
|
||||
// reference the Dompdf namespace
|
||||
use Dompdf\Dompdf;
|
||||
|
||||
// instantiate and use the dompdf class
|
||||
$dompdf = new Dompdf();
|
||||
$dompdf->loadHtml('hello world');
|
||||
|
||||
// (Optional) Setup the paper size and orientation
|
||||
$dompdf->setPaper('A4', 'landscape');
|
||||
|
||||
// Render the HTML as PDF
|
||||
$dompdf->render();
|
||||
|
||||
// Output the generated PDF to Browser
|
||||
$dompdf->stream();
|
||||
```
|
||||
|
||||
### Setting Options
|
||||
|
||||
Set options during dompdf instantiation:
|
||||
|
||||
```php
|
||||
use Dompdf\Dompdf;
|
||||
use Dompdf\Options;
|
||||
|
||||
$options = new Options();
|
||||
$options->set('defaultFont', 'Courier');
|
||||
$dompdf = new Dompdf($options);
|
||||
```
|
||||
|
||||
or at run time
|
||||
|
||||
```php
|
||||
use Dompdf\Dompdf;
|
||||
|
||||
$dompdf = new Dompdf();
|
||||
$options = $dompdf->getOptions();
|
||||
$options->setDefaultFont('Courier');
|
||||
$dompdf->setOptions($options);
|
||||
```
|
||||
|
||||
See [Dompdf\Options](src/Options.php) for a list of available options.
|
||||
|
||||
### Resource Reference Requirements
|
||||
|
||||
In order to protect potentially sensitive information Dompdf imposes
|
||||
restrictions on files referenced from the local file system or the web.
|
||||
|
||||
Files accessed through web-based protocols have the following requirements:
|
||||
* The Dompdf option "isRemoteEnabled" must be set to "true"
|
||||
* PHP must either have the curl extension enabled or the
|
||||
allow_url_fopen setting set to true
|
||||
|
||||
Files accessed through the local file system have the following requirement:
|
||||
* The file must fall within the path(s) specified for the Dompdf "chroot" option
|
||||
|
||||
## Limitations (Known Issues)
|
||||
|
||||
* Table cells are not pageable, meaning a table row must fit on a single page.
|
||||
* Elements are rendered on the active page when they are parsed.
|
||||
* Embedding "raw" SVG's (`<svg><path...></svg>`) isn't working yet, you need to
|
||||
either link to an external SVG file, or use a DataURI like this:
|
||||
```php
|
||||
$html = '<img src="data:image/svg+xml;base64,' . base64_encode($svg) . '" ...>';
|
||||
```
|
||||
Watch https://github.com/dompdf/dompdf/issues/320 for progress
|
||||
* Does not support CSS flexbox.
|
||||
* Does not support CSS Grid.
|
||||
---
|
||||
|
||||
[](http://goo.gl/DSvWf)
|
||||
|
||||
*If you find this project useful, please consider making a donation.
|
||||
Any funds donated will be used to help further development on this project.)*
|
||||
1
pirp/vendor/dompdf/dompdf/VERSION
vendored
Normal file
1
pirp/vendor/dompdf/dompdf/VERSION
vendored
Normal file
@@ -0,0 +1 @@
|
||||
2.0.8
|
||||
47
pirp/vendor/dompdf/dompdf/composer.json
vendored
Normal file
47
pirp/vendor/dompdf/dompdf/composer.json
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "dompdf/dompdf",
|
||||
"type": "library",
|
||||
"description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
|
||||
"homepage": "https://github.com/dompdf/dompdf",
|
||||
"license": "LGPL-2.1",
|
||||
"authors": [
|
||||
{
|
||||
"name": "The Dompdf Community",
|
||||
"homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Dompdf\\": "src/"
|
||||
},
|
||||
"classmap": [
|
||||
"lib/"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Dompdf\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0",
|
||||
"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"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-json": "*",
|
||||
"ext-zip": "*",
|
||||
"phpunit/phpunit": "^7.5 || ^8 || ^9",
|
||||
"squizlabs/php_codesniffer": "^3.5",
|
||||
"mockery/mockery": "^1.3"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-gd": "Needed to process images",
|
||||
"ext-imagick": "Improves image processing performance",
|
||||
"ext-gmagick": "Improves image processing performance",
|
||||
"ext-zlib": "Needed for pdf stream compression"
|
||||
}
|
||||
}
|
||||
6501
pirp/vendor/dompdf/dompdf/lib/Cpdf.php
vendored
Normal file
6501
pirp/vendor/dompdf/dompdf/lib/Cpdf.php
vendored
Normal file
File diff suppressed because it is too large
Load Diff
344
pirp/vendor/dompdf/dompdf/lib/fonts/Courier-Bold.afm
vendored
Normal file
344
pirp/vendor/dompdf/dompdf/lib/fonts/Courier-Bold.afm
vendored
Normal file
@@ -0,0 +1,344 @@
|
||||
StartFontMetrics 4.1
|
||||
Comment Copyright (c) 1989, 1990, 1991, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved.
|
||||
Comment Creation Date: Mon Jun 23 16:28:00 0:00:00
|
||||
Comment UniqueID 43048
|
||||
Comment VMusage 41139 52164
|
||||
FontName Courier-Bold
|
||||
FullName Courier Bold
|
||||
FamilyName Courier
|
||||
Weight Bold
|
||||
ItalicAngle 0
|
||||
IsFixedPitch true
|
||||
CharacterSet ExtendedRoman
|
||||
FontBBox -113 -250 749 801
|
||||
UnderlinePosition -100
|
||||
UnderlineThickness 50
|
||||
Version 003.000
|
||||
Notice Copyright (c) 1989, 1990, 1991, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved.
|
||||
EncodingScheme WinAnsiEncoding
|
||||
CapHeight 562
|
||||
XHeight 439
|
||||
Ascender 629
|
||||
Descender -157
|
||||
StdHW 84
|
||||
StdVW 106
|
||||
StartCharMetrics 317
|
||||
C 32 ; WX 600 ; N space ; B 0 0 0 0 ;
|
||||
C 160 ; WX 600 ; N space ; B 0 0 0 0 ;
|
||||
C 33 ; WX 600 ; N exclam ; B 202 -15 398 572 ;
|
||||
C 34 ; WX 600 ; N quotedbl ; B 135 277 465 562 ;
|
||||
C 35 ; WX 600 ; N numbersign ; B 56 -45 544 651 ;
|
||||
C 36 ; WX 600 ; N dollar ; B 82 -126 519 666 ;
|
||||
C 37 ; WX 600 ; N percent ; B 5 -15 595 616 ;
|
||||
C 38 ; WX 600 ; N ampersand ; B 36 -15 546 543 ;
|
||||
C 146 ; WX 600 ; N quoteright ; B 171 277 423 562 ;
|
||||
C 40 ; WX 600 ; N parenleft ; B 219 -102 461 616 ;
|
||||
C 41 ; WX 600 ; N parenright ; B 139 -102 381 616 ;
|
||||
C 42 ; WX 600 ; N asterisk ; B 91 219 509 601 ;
|
||||
C 43 ; WX 600 ; N plus ; B 71 39 529 478 ;
|
||||
C 44 ; WX 600 ; N comma ; B 123 -111 393 174 ;
|
||||
C 45 ; WX 600 ; N hyphen ; B 100 203 500 313 ;
|
||||
C 173 ; WX 600 ; N hyphen ; B 100 203 500 313 ;
|
||||
C 46 ; WX 600 ; N period ; B 192 -15 408 171 ;
|
||||
C 47 ; WX 600 ; N slash ; B 98 -77 502 626 ;
|
||||
C 48 ; WX 600 ; N zero ; B 87 -15 513 616 ;
|
||||
C 49 ; WX 600 ; N one ; B 81 0 539 616 ;
|
||||
C 50 ; WX 600 ; N two ; B 61 0 499 616 ;
|
||||
C 51 ; WX 600 ; N three ; B 63 -15 501 616 ;
|
||||
C 52 ; WX 600 ; N four ; B 53 0 507 616 ;
|
||||
C 53 ; WX 600 ; N five ; B 70 -15 521 601 ;
|
||||
C 54 ; WX 600 ; N six ; B 90 -15 521 616 ;
|
||||
C 55 ; WX 600 ; N seven ; B 55 0 494 601 ;
|
||||
C 56 ; WX 600 ; N eight ; B 83 -15 517 616 ;
|
||||
C 57 ; WX 600 ; N nine ; B 79 -15 510 616 ;
|
||||
C 58 ; WX 600 ; N colon ; B 191 -15 407 425 ;
|
||||
C 59 ; WX 600 ; N semicolon ; B 123 -111 408 425 ;
|
||||
C 60 ; WX 600 ; N less ; B 66 15 523 501 ;
|
||||
C 61 ; WX 600 ; N equal ; B 71 118 529 398 ;
|
||||
C 62 ; WX 600 ; N greater ; B 77 15 534 501 ;
|
||||
C 63 ; WX 600 ; N question ; B 98 -14 501 580 ;
|
||||
C 64 ; WX 600 ; N at ; B 16 -15 584 616 ;
|
||||
C 65 ; WX 600 ; N A ; B -9 0 609 562 ;
|
||||
C 66 ; WX 600 ; N B ; B 30 0 573 562 ;
|
||||
C 67 ; WX 600 ; N C ; B 22 -18 560 580 ;
|
||||
C 68 ; WX 600 ; N D ; B 30 0 594 562 ;
|
||||
C 69 ; WX 600 ; N E ; B 25 0 560 562 ;
|
||||
C 70 ; WX 600 ; N F ; B 39 0 570 562 ;
|
||||
C 71 ; WX 600 ; N G ; B 22 -18 594 580 ;
|
||||
C 72 ; WX 600 ; N H ; B 20 0 580 562 ;
|
||||
C 73 ; WX 600 ; N I ; B 77 0 523 562 ;
|
||||
C 74 ; WX 600 ; N J ; B 37 -18 601 562 ;
|
||||
C 75 ; WX 600 ; N K ; B 21 0 599 562 ;
|
||||
C 76 ; WX 600 ; N L ; B 39 0 578 562 ;
|
||||
C 77 ; WX 600 ; N M ; B -2 0 602 562 ;
|
||||
C 78 ; WX 600 ; N N ; B 8 -12 610 562 ;
|
||||
C 79 ; WX 600 ; N O ; B 22 -18 578 580 ;
|
||||
C 80 ; WX 600 ; N P ; B 48 0 559 562 ;
|
||||
C 81 ; WX 600 ; N Q ; B 32 -138 578 580 ;
|
||||
C 82 ; WX 600 ; N R ; B 24 0 599 562 ;
|
||||
C 83 ; WX 600 ; N S ; B 47 -22 553 582 ;
|
||||
C 84 ; WX 600 ; N T ; B 21 0 579 562 ;
|
||||
C 85 ; WX 600 ; N U ; B 4 -18 596 562 ;
|
||||
C 86 ; WX 600 ; N V ; B -13 0 613 562 ;
|
||||
C 87 ; WX 600 ; N W ; B -18 0 618 562 ;
|
||||
C 88 ; WX 600 ; N X ; B 12 0 588 562 ;
|
||||
C 89 ; WX 600 ; N Y ; B 12 0 589 562 ;
|
||||
C 90 ; WX 600 ; N Z ; B 62 0 539 562 ;
|
||||
C 91 ; WX 600 ; N bracketleft ; B 245 -102 475 616 ;
|
||||
C 92 ; WX 600 ; N backslash ; B 99 -77 503 626 ;
|
||||
C 93 ; WX 600 ; N bracketright ; B 125 -102 355 616 ;
|
||||
C 94 ; WX 600 ; N asciicircum ; B 108 250 492 616 ;
|
||||
C 95 ; WX 600 ; N underscore ; B 0 -125 600 -75 ;
|
||||
C 145 ; WX 600 ; N quoteleft ; B 178 277 428 562 ;
|
||||
C 97 ; WX 600 ; N a ; B 35 -15 570 454 ;
|
||||
C 98 ; WX 600 ; N b ; B 0 -15 584 626 ;
|
||||
C 99 ; WX 600 ; N c ; B 40 -15 545 459 ;
|
||||
C 100 ; WX 600 ; N d ; B 20 -15 591 626 ;
|
||||
C 101 ; WX 600 ; N e ; B 40 -15 563 454 ;
|
||||
C 102 ; WX 600 ; N f ; B 83 0 547 626 ; L i fi ; L l fl ;
|
||||
C 103 ; WX 600 ; N g ; B 30 -146 580 454 ;
|
||||
C 104 ; WX 600 ; N h ; B 5 0 592 626 ;
|
||||
C 105 ; WX 600 ; N i ; B 77 0 523 658 ;
|
||||
C 106 ; WX 600 ; N j ; B 63 -146 440 658 ;
|
||||
C 107 ; WX 600 ; N k ; B 20 0 585 626 ;
|
||||
C 108 ; WX 600 ; N l ; B 77 0 523 626 ;
|
||||
C 109 ; WX 600 ; N m ; B -22 0 626 454 ;
|
||||
C 110 ; WX 600 ; N n ; B 18 0 592 454 ;
|
||||
C 111 ; WX 600 ; N o ; B 30 -15 570 454 ;
|
||||
C 112 ; WX 600 ; N p ; B -1 -142 570 454 ;
|
||||
C 113 ; WX 600 ; N q ; B 20 -142 591 454 ;
|
||||
C 114 ; WX 600 ; N r ; B 47 0 580 454 ;
|
||||
C 115 ; WX 600 ; N s ; B 68 -17 535 459 ;
|
||||
C 116 ; WX 600 ; N t ; B 47 -15 532 562 ;
|
||||
C 117 ; WX 600 ; N u ; B -1 -15 569 439 ;
|
||||
C 118 ; WX 600 ; N v ; B -1 0 601 439 ;
|
||||
C 119 ; WX 600 ; N w ; B -18 0 618 439 ;
|
||||
C 120 ; WX 600 ; N x ; B 6 0 594 439 ;
|
||||
C 121 ; WX 600 ; N y ; B -4 -142 601 439 ;
|
||||
C 122 ; WX 600 ; N z ; B 81 0 520 439 ;
|
||||
C 123 ; WX 600 ; N braceleft ; B 160 -102 464 616 ;
|
||||
C 124 ; WX 600 ; N bar ; B 255 -250 345 750 ;
|
||||
C 125 ; WX 600 ; N braceright ; B 136 -102 440 616 ;
|
||||
C 126 ; WX 600 ; N asciitilde ; B 71 153 530 356 ;
|
||||
C 161 ; WX 600 ; N exclamdown ; B 202 -146 398 449 ;
|
||||
C 162 ; WX 600 ; N cent ; B 66 -49 518 614 ;
|
||||
C 163 ; WX 600 ; N sterling ; B 72 -28 558 611 ;
|
||||
C -1 ; WX 600 ; N fraction ; B 25 -60 576 661 ;
|
||||
C 165 ; WX 600 ; N yen ; B 10 0 590 562 ;
|
||||
C 131 ; WX 600 ; N florin ; B -30 -131 572 616 ;
|
||||
C 167 ; WX 600 ; N section ; B 83 -70 517 580 ;
|
||||
C 164 ; WX 600 ; N currency ; B 54 49 546 517 ;
|
||||
C 39 ; WX 600 ; N quotesingle ; B 227 277 373 562 ;
|
||||
C 147 ; WX 600 ; N quotedblleft ; B 71 277 535 562 ;
|
||||
C 171 ; WX 600 ; N guillemotleft ; B 8 70 553 446 ;
|
||||
C 139 ; WX 600 ; N guilsinglleft ; B 141 70 459 446 ;
|
||||
C 155 ; WX 600 ; N guilsinglright ; B 141 70 459 446 ;
|
||||
C -1 ; WX 600 ; N fi ; B 12 0 593 626 ;
|
||||
C -1 ; WX 600 ; N fl ; B 12 0 593 626 ;
|
||||
C 150 ; WX 600 ; N endash ; B 65 203 535 313 ;
|
||||
C 134 ; WX 600 ; N dagger ; B 106 -70 494 580 ;
|
||||
C 135 ; WX 600 ; N daggerdbl ; B 106 -70 494 580 ;
|
||||
C 183 ; WX 600 ; N periodcentered ; B 196 165 404 351 ;
|
||||
C 182 ; WX 600 ; N paragraph ; B 6 -70 576 580 ;
|
||||
C 149 ; WX 600 ; N bullet ; B 140 132 460 430 ;
|
||||
C 130 ; WX 600 ; N quotesinglbase ; B 175 -142 427 143 ;
|
||||
C 132 ; WX 600 ; N quotedblbase ; B 65 -142 529 143 ;
|
||||
C 148 ; WX 600 ; N quotedblright ; B 61 277 525 562 ;
|
||||
C 187 ; WX 600 ; N guillemotright ; B 47 70 592 446 ;
|
||||
C 133 ; WX 600 ; N ellipsis ; B 26 -15 574 116 ;
|
||||
C 137 ; WX 600 ; N perthousand ; B -113 -15 713 616 ;
|
||||
C 191 ; WX 600 ; N questiondown ; B 99 -146 502 449 ;
|
||||
C 96 ; WX 600 ; N grave ; B 132 508 395 661 ;
|
||||
C 180 ; WX 600 ; N acute ; B 205 508 468 661 ;
|
||||
C 136 ; WX 600 ; N circumflex ; B 103 483 497 657 ;
|
||||
C 152 ; WX 600 ; N tilde ; B 89 493 512 636 ;
|
||||
C 175 ; WX 600 ; N macron ; B 88 505 512 585 ;
|
||||
C -1 ; WX 600 ; N breve ; B 83 468 517 631 ;
|
||||
C -1 ; WX 600 ; N dotaccent ; B 230 498 370 638 ;
|
||||
C 168 ; WX 600 ; N dieresis ; B 128 498 472 638 ;
|
||||
C -1 ; WX 600 ; N ring ; B 198 481 402 678 ;
|
||||
C 184 ; WX 600 ; N cedilla ; B 205 -206 387 0 ;
|
||||
C -1 ; WX 600 ; N hungarumlaut ; B 68 488 588 661 ;
|
||||
C -1 ; WX 600 ; N ogonek ; B 169 -199 400 0 ;
|
||||
C -1 ; WX 600 ; N caron ; B 103 493 497 667 ;
|
||||
C 151 ; WX 600 ; N emdash ; B -10 203 610 313 ;
|
||||
C 198 ; WX 600 ; N AE ; B -29 0 602 562 ;
|
||||
C 170 ; WX 600 ; N ordfeminine ; B 147 196 453 580 ;
|
||||
C -1 ; WX 600 ; N Lslash ; B 39 0 578 562 ;
|
||||
C 216 ; WX 600 ; N Oslash ; B 22 -22 578 584 ;
|
||||
C 140 ; WX 600 ; N OE ; B -25 0 595 562 ;
|
||||
C 186 ; WX 600 ; N ordmasculine ; B 147 196 453 580 ;
|
||||
C 230 ; WX 600 ; N ae ; B -4 -15 601 454 ;
|
||||
C -1 ; WX 600 ; N dotlessi ; B 77 0 523 439 ;
|
||||
C -1 ; WX 600 ; N lslash ; B 77 0 523 626 ;
|
||||
C 248 ; WX 600 ; N oslash ; B 30 -24 570 463 ;
|
||||
C 156 ; WX 600 ; N oe ; B -18 -15 611 454 ;
|
||||
C 223 ; WX 600 ; N germandbls ; B 22 -15 596 626 ;
|
||||
C 207 ; WX 600 ; N Idieresis ; B 77 0 523 761 ;
|
||||
C 233 ; WX 600 ; N eacute ; B 40 -15 563 661 ;
|
||||
C -1 ; WX 600 ; N abreve ; B 35 -15 570 661 ;
|
||||
C -1 ; WX 600 ; N uhungarumlaut ; B -1 -15 628 661 ;
|
||||
C -1 ; WX 600 ; N ecaron ; B 40 -15 563 667 ;
|
||||
C 159 ; WX 600 ; N Ydieresis ; B 12 0 589 761 ;
|
||||
C 247 ; WX 600 ; N divide ; B 71 16 529 500 ;
|
||||
C 221 ; WX 600 ; N Yacute ; B 12 0 589 784 ;
|
||||
C 194 ; WX 600 ; N Acircumflex ; B -9 0 609 780 ;
|
||||
C 225 ; WX 600 ; N aacute ; B 35 -15 570 661 ;
|
||||
C 219 ; WX 600 ; N Ucircumflex ; B 4 -18 596 780 ;
|
||||
C 253 ; WX 600 ; N yacute ; B -4 -142 601 661 ;
|
||||
C -1 ; WX 600 ; N scommaaccent ; B 68 -250 535 459 ;
|
||||
C 234 ; WX 600 ; N ecircumflex ; B 40 -15 563 657 ;
|
||||
C -1 ; WX 600 ; N Uring ; B 4 -18 596 801 ;
|
||||
C 220 ; WX 600 ; N Udieresis ; B 4 -18 596 761 ;
|
||||
C -1 ; WX 600 ; N aogonek ; B 35 -199 586 454 ;
|
||||
C 218 ; WX 600 ; N Uacute ; B 4 -18 596 784 ;
|
||||
C -1 ; WX 600 ; N uogonek ; B -1 -199 585 439 ;
|
||||
C 203 ; WX 600 ; N Edieresis ; B 25 0 560 761 ;
|
||||
C -1 ; WX 600 ; N Dcroat ; B 30 0 594 562 ;
|
||||
C -1 ; WX 600 ; N commaaccent ; B 205 -250 397 -57 ;
|
||||
C 169 ; WX 600 ; N copyright ; B 0 -18 600 580 ;
|
||||
C -1 ; WX 600 ; N Emacron ; B 25 0 560 708 ;
|
||||
C -1 ; WX 600 ; N ccaron ; B 40 -15 545 667 ;
|
||||
C 229 ; WX 600 ; N aring ; B 35 -15 570 678 ;
|
||||
C -1 ; WX 600 ; N Ncommaaccent ; B 8 -250 610 562 ;
|
||||
C -1 ; WX 600 ; N lacute ; B 77 0 523 801 ;
|
||||
C 224 ; WX 600 ; N agrave ; B 35 -15 570 661 ;
|
||||
C -1 ; WX 600 ; N Tcommaaccent ; B 21 -250 579 562 ;
|
||||
C -1 ; WX 600 ; N Cacute ; B 22 -18 560 784 ;
|
||||
C 227 ; WX 600 ; N atilde ; B 35 -15 570 636 ;
|
||||
C -1 ; WX 600 ; N Edotaccent ; B 25 0 560 761 ;
|
||||
C 154 ; WX 600 ; N scaron ; B 68 -17 535 667 ;
|
||||
C -1 ; WX 600 ; N scedilla ; B 68 -206 535 459 ;
|
||||
C 237 ; WX 600 ; N iacute ; B 77 0 523 661 ;
|
||||
C -1 ; WX 600 ; N lozenge ; B 66 0 534 740 ;
|
||||
C -1 ; WX 600 ; N Rcaron ; B 24 0 599 790 ;
|
||||
C -1 ; WX 600 ; N Gcommaaccent ; B 22 -250 594 580 ;
|
||||
C 251 ; WX 600 ; N ucircumflex ; B -1 -15 569 657 ;
|
||||
C 226 ; WX 600 ; N acircumflex ; B 35 -15 570 657 ;
|
||||
C -1 ; WX 600 ; N Amacron ; B -9 0 609 708 ;
|
||||
C -1 ; WX 600 ; N rcaron ; B 47 0 580 667 ;
|
||||
C 231 ; WX 600 ; N ccedilla ; B 40 -206 545 459 ;
|
||||
C -1 ; WX 600 ; N Zdotaccent ; B 62 0 539 761 ;
|
||||
C 222 ; WX 600 ; N Thorn ; B 48 0 557 562 ;
|
||||
C -1 ; WX 600 ; N Omacron ; B 22 -18 578 708 ;
|
||||
C -1 ; WX 600 ; N Racute ; B 24 0 599 784 ;
|
||||
C -1 ; WX 600 ; N Sacute ; B 47 -22 553 784 ;
|
||||
C -1 ; WX 600 ; N dcaron ; B 20 -15 727 626 ;
|
||||
C -1 ; WX 600 ; N Umacron ; B 4 -18 596 708 ;
|
||||
C -1 ; WX 600 ; N uring ; B -1 -15 569 678 ;
|
||||
C 179 ; WX 600 ; N threesuperior ; B 138 222 433 616 ;
|
||||
C 210 ; WX 600 ; N Ograve ; B 22 -18 578 784 ;
|
||||
C 192 ; WX 600 ; N Agrave ; B -9 0 609 784 ;
|
||||
C -1 ; WX 600 ; N Abreve ; B -9 0 609 784 ;
|
||||
C 215 ; WX 600 ; N multiply ; B 81 39 520 478 ;
|
||||
C 250 ; WX 600 ; N uacute ; B -1 -15 569 661 ;
|
||||
C -1 ; WX 600 ; N Tcaron ; B 21 0 579 790 ;
|
||||
C -1 ; WX 600 ; N partialdiff ; B 63 -38 537 728 ;
|
||||
C 255 ; WX 600 ; N ydieresis ; B -4 -142 601 638 ;
|
||||
C -1 ; WX 600 ; N Nacute ; B 8 -12 610 784 ;
|
||||
C 238 ; WX 600 ; N icircumflex ; B 73 0 523 657 ;
|
||||
C 202 ; WX 600 ; N Ecircumflex ; B 25 0 560 780 ;
|
||||
C 228 ; WX 600 ; N adieresis ; B 35 -15 570 638 ;
|
||||
C 235 ; WX 600 ; N edieresis ; B 40 -15 563 638 ;
|
||||
C -1 ; WX 600 ; N cacute ; B 40 -15 545 661 ;
|
||||
C -1 ; WX 600 ; N nacute ; B 18 0 592 661 ;
|
||||
C -1 ; WX 600 ; N umacron ; B -1 -15 569 585 ;
|
||||
C -1 ; WX 600 ; N Ncaron ; B 8 -12 610 790 ;
|
||||
C 205 ; WX 600 ; N Iacute ; B 77 0 523 784 ;
|
||||
C 177 ; WX 600 ; N plusminus ; B 71 24 529 515 ;
|
||||
C 166 ; WX 600 ; N brokenbar ; B 255 -175 345 675 ;
|
||||
C 174 ; WX 600 ; N registered ; B 0 -18 600 580 ;
|
||||
C -1 ; WX 600 ; N Gbreve ; B 22 -18 594 784 ;
|
||||
C -1 ; WX 600 ; N Idotaccent ; B 77 0 523 761 ;
|
||||
C -1 ; WX 600 ; N summation ; B 15 -10 586 706 ;
|
||||
C 200 ; WX 600 ; N Egrave ; B 25 0 560 784 ;
|
||||
C -1 ; WX 600 ; N racute ; B 47 0 580 661 ;
|
||||
C -1 ; WX 600 ; N omacron ; B 30 -15 570 585 ;
|
||||
C -1 ; WX 600 ; N Zacute ; B 62 0 539 784 ;
|
||||
C 142 ; WX 600 ; N Zcaron ; B 62 0 539 790 ;
|
||||
C -1 ; WX 600 ; N greaterequal ; B 26 0 523 696 ;
|
||||
C 208 ; WX 600 ; N Eth ; B 30 0 594 562 ;
|
||||
C 199 ; WX 600 ; N Ccedilla ; B 22 -206 560 580 ;
|
||||
C -1 ; WX 600 ; N lcommaaccent ; B 77 -250 523 626 ;
|
||||
C -1 ; WX 600 ; N tcaron ; B 47 -15 532 703 ;
|
||||
C -1 ; WX 600 ; N eogonek ; B 40 -199 563 454 ;
|
||||
C -1 ; WX 600 ; N Uogonek ; B 4 -199 596 562 ;
|
||||
C 193 ; WX 600 ; N Aacute ; B -9 0 609 784 ;
|
||||
C 196 ; WX 600 ; N Adieresis ; B -9 0 609 761 ;
|
||||
C 232 ; WX 600 ; N egrave ; B 40 -15 563 661 ;
|
||||
C -1 ; WX 600 ; N zacute ; B 81 0 520 661 ;
|
||||
C -1 ; WX 600 ; N iogonek ; B 77 -199 523 658 ;
|
||||
C 211 ; WX 600 ; N Oacute ; B 22 -18 578 784 ;
|
||||
C 243 ; WX 600 ; N oacute ; B 30 -15 570 661 ;
|
||||
C -1 ; WX 600 ; N amacron ; B 35 -15 570 585 ;
|
||||
C -1 ; WX 600 ; N sacute ; B 68 -17 535 661 ;
|
||||
C 239 ; WX 600 ; N idieresis ; B 77 0 523 618 ;
|
||||
C 212 ; WX 600 ; N Ocircumflex ; B 22 -18 578 780 ;
|
||||
C 217 ; WX 600 ; N Ugrave ; B 4 -18 596 784 ;
|
||||
C -1 ; WX 600 ; N Delta ; B 6 0 594 688 ;
|
||||
C 254 ; WX 600 ; N thorn ; B -14 -142 570 626 ;
|
||||
C 178 ; WX 600 ; N twosuperior ; B 143 230 436 616 ;
|
||||
C 214 ; WX 600 ; N Odieresis ; B 22 -18 578 761 ;
|
||||
C 181 ; WX 600 ; N mu ; B -1 -142 569 439 ;
|
||||
C 236 ; WX 600 ; N igrave ; B 77 0 523 661 ;
|
||||
C -1 ; WX 600 ; N ohungarumlaut ; B 30 -15 668 661 ;
|
||||
C -1 ; WX 600 ; N Eogonek ; B 25 -199 576 562 ;
|
||||
C -1 ; WX 600 ; N dcroat ; B 20 -15 591 626 ;
|
||||
C 190 ; WX 600 ; N threequarters ; B -47 -60 648 661 ;
|
||||
C -1 ; WX 600 ; N Scedilla ; B 47 -206 553 582 ;
|
||||
C -1 ; WX 600 ; N lcaron ; B 77 0 597 626 ;
|
||||
C -1 ; WX 600 ; N Kcommaaccent ; B 21 -250 599 562 ;
|
||||
C -1 ; WX 600 ; N Lacute ; B 39 0 578 784 ;
|
||||
C 153 ; WX 600 ; N trademark ; B -9 230 749 562 ;
|
||||
C -1 ; WX 600 ; N edotaccent ; B 40 -15 563 638 ;
|
||||
C 204 ; WX 600 ; N Igrave ; B 77 0 523 784 ;
|
||||
C -1 ; WX 600 ; N Imacron ; B 77 0 523 708 ;
|
||||
C -1 ; WX 600 ; N Lcaron ; B 39 0 637 562 ;
|
||||
C 189 ; WX 600 ; N onehalf ; B -47 -60 648 661 ;
|
||||
C -1 ; WX 600 ; N lessequal ; B 26 0 523 696 ;
|
||||
C 244 ; WX 600 ; N ocircumflex ; B 30 -15 570 657 ;
|
||||
C 241 ; WX 600 ; N ntilde ; B 18 0 592 636 ;
|
||||
C -1 ; WX 600 ; N Uhungarumlaut ; B 4 -18 638 784 ;
|
||||
C 201 ; WX 600 ; N Eacute ; B 25 0 560 784 ;
|
||||
C -1 ; WX 600 ; N emacron ; B 40 -15 563 585 ;
|
||||
C -1 ; WX 600 ; N gbreve ; B 30 -146 580 661 ;
|
||||
C 188 ; WX 600 ; N onequarter ; B -56 -60 656 661 ;
|
||||
C 138 ; WX 600 ; N Scaron ; B 47 -22 553 790 ;
|
||||
C -1 ; WX 600 ; N Scommaaccent ; B 47 -250 553 582 ;
|
||||
C -1 ; WX 600 ; N Ohungarumlaut ; B 22 -18 628 784 ;
|
||||
C 176 ; WX 600 ; N degree ; B 86 243 474 616 ;
|
||||
C 242 ; WX 600 ; N ograve ; B 30 -15 570 661 ;
|
||||
C -1 ; WX 600 ; N Ccaron ; B 22 -18 560 790 ;
|
||||
C 249 ; WX 600 ; N ugrave ; B -1 -15 569 661 ;
|
||||
C -1 ; WX 600 ; N radical ; B -19 -104 473 778 ;
|
||||
C -1 ; WX 600 ; N Dcaron ; B 30 0 594 790 ;
|
||||
C -1 ; WX 600 ; N rcommaaccent ; B 47 -250 580 454 ;
|
||||
C 209 ; WX 600 ; N Ntilde ; B 8 -12 610 759 ;
|
||||
C 245 ; WX 600 ; N otilde ; B 30 -15 570 636 ;
|
||||
C -1 ; WX 600 ; N Rcommaaccent ; B 24 -250 599 562 ;
|
||||
C -1 ; WX 600 ; N Lcommaaccent ; B 39 -250 578 562 ;
|
||||
C 195 ; WX 600 ; N Atilde ; B -9 0 609 759 ;
|
||||
C -1 ; WX 600 ; N Aogonek ; B -9 -199 625 562 ;
|
||||
C 197 ; WX 600 ; N Aring ; B -9 0 609 801 ;
|
||||
C 213 ; WX 600 ; N Otilde ; B 22 -18 578 759 ;
|
||||
C -1 ; WX 600 ; N zdotaccent ; B 81 0 520 638 ;
|
||||
C -1 ; WX 600 ; N Ecaron ; B 25 0 560 790 ;
|
||||
C -1 ; WX 600 ; N Iogonek ; B 77 -199 523 562 ;
|
||||
C -1 ; WX 600 ; N kcommaaccent ; B 20 -250 585 626 ;
|
||||
C -1 ; WX 600 ; N minus ; B 71 203 529 313 ;
|
||||
C 206 ; WX 600 ; N Icircumflex ; B 77 0 523 780 ;
|
||||
C -1 ; WX 600 ; N ncaron ; B 18 0 592 667 ;
|
||||
C -1 ; WX 600 ; N tcommaaccent ; B 47 -250 532 562 ;
|
||||
C 172 ; WX 600 ; N logicalnot ; B 71 103 529 413 ;
|
||||
C 246 ; WX 600 ; N odieresis ; B 30 -15 570 638 ;
|
||||
C 252 ; WX 600 ; N udieresis ; B -1 -15 569 638 ;
|
||||
C -1 ; WX 600 ; N notequal ; B 12 -47 537 563 ;
|
||||
C -1 ; WX 600 ; N gcommaaccent ; B 30 -146 580 714 ;
|
||||
C 240 ; WX 600 ; N eth ; B 58 -27 543 626 ;
|
||||
C 158 ; WX 600 ; N zcaron ; B 81 0 520 667 ;
|
||||
C -1 ; WX 600 ; N ncommaaccent ; B 18 -250 592 454 ;
|
||||
C 185 ; WX 600 ; N onesuperior ; B 153 230 447 616 ;
|
||||
C -1 ; WX 600 ; N imacron ; B 77 0 523 585 ;
|
||||
C 128 ; WX 600 ; N Euro ; B 0 0 0 0 ;
|
||||
EndCharMetrics
|
||||
EndFontMetrics
|
||||
344
pirp/vendor/dompdf/dompdf/lib/fonts/Courier-BoldOblique.afm
vendored
Normal file
344
pirp/vendor/dompdf/dompdf/lib/fonts/Courier-BoldOblique.afm
vendored
Normal file
@@ -0,0 +1,344 @@
|
||||
StartFontMetrics 4.1
|
||||
Comment Copyright (c) 1989, 1990, 1991, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved.
|
||||
Comment Creation Date: Mon Jun 23 16:28:46 0:00:00
|
||||
Comment UniqueID 43049
|
||||
Comment VMusage 17529 79244
|
||||
FontName Courier-BoldOblique
|
||||
FullName Courier Bold Oblique
|
||||
FamilyName Courier
|
||||
Weight Bold
|
||||
ItalicAngle -12
|
||||
IsFixedPitch true
|
||||
CharacterSet ExtendedRoman
|
||||
FontBBox -57 -250 869 801
|
||||
UnderlinePosition -100
|
||||
UnderlineThickness 50
|
||||
Version 3
|
||||
Notice Copyright (c) 1989, 1990, 1991, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved.
|
||||
EncodingScheme WinAnsiEncoding
|
||||
CapHeight 562
|
||||
XHeight 439
|
||||
Ascender 629
|
||||
Descender -157
|
||||
StdHW 84
|
||||
StdVW 106
|
||||
StartCharMetrics 317
|
||||
C 32 ; WX 600 ; N space ; B 0 0 0 0 ;
|
||||
C 160 ; WX 600 ; N space ; B 0 0 0 0 ;
|
||||
C 33 ; WX 600 ; N exclam ; B 215 -15 495 572 ;
|
||||
C 34 ; WX 600 ; N quotedbl ; B 211 277 585 562 ;
|
||||
C 35 ; WX 600 ; N numbersign ; B 88 -45 641 651 ;
|
||||
C 36 ; WX 600 ; N dollar ; B 87 -126 630 666 ;
|
||||
C 37 ; WX 600 ; N percent ; B 101 -15 625 616 ;
|
||||
C 38 ; WX 600 ; N ampersand ; B 61 -15 595 543 ;
|
||||
C 146 ; WX 600 ; N quoteright ; B 229 277 543 562 ;
|
||||
C 40 ; WX 600 ; N parenleft ; B 265 -102 592 616 ;
|
||||
C 41 ; WX 600 ; N parenright ; B 117 -102 444 616 ;
|
||||
C 42 ; WX 600 ; N asterisk ; B 179 219 598 601 ;
|
||||
C 43 ; WX 600 ; N plus ; B 114 39 596 478 ;
|
||||
C 44 ; WX 600 ; N comma ; B 99 -111 430 174 ;
|
||||
C 45 ; WX 600 ; N hyphen ; B 143 203 567 313 ;
|
||||
C 173 ; WX 600 ; N hyphen ; B 143 203 567 313 ;
|
||||
C 46 ; WX 600 ; N period ; B 206 -15 427 171 ;
|
||||
C 47 ; WX 600 ; N slash ; B 90 -77 626 626 ;
|
||||
C 48 ; WX 600 ; N zero ; B 135 -15 593 616 ;
|
||||
C 49 ; WX 600 ; N one ; B 93 0 562 616 ;
|
||||
C 50 ; WX 600 ; N two ; B 61 0 594 616 ;
|
||||
C 51 ; WX 600 ; N three ; B 71 -15 571 616 ;
|
||||
C 52 ; WX 600 ; N four ; B 81 0 559 616 ;
|
||||
C 53 ; WX 600 ; N five ; B 77 -15 621 601 ;
|
||||
C 54 ; WX 600 ; N six ; B 135 -15 652 616 ;
|
||||
C 55 ; WX 600 ; N seven ; B 147 0 622 601 ;
|
||||
C 56 ; WX 600 ; N eight ; B 115 -15 604 616 ;
|
||||
C 57 ; WX 600 ; N nine ; B 75 -15 592 616 ;
|
||||
C 58 ; WX 600 ; N colon ; B 205 -15 480 425 ;
|
||||
C 59 ; WX 600 ; N semicolon ; B 99 -111 481 425 ;
|
||||
C 60 ; WX 600 ; N less ; B 120 15 613 501 ;
|
||||
C 61 ; WX 600 ; N equal ; B 96 118 614 398 ;
|
||||
C 62 ; WX 600 ; N greater ; B 97 15 589 501 ;
|
||||
C 63 ; WX 600 ; N question ; B 183 -14 592 580 ;
|
||||
C 64 ; WX 600 ; N at ; B 65 -15 642 616 ;
|
||||
C 65 ; WX 600 ; N A ; B -9 0 632 562 ;
|
||||
C 66 ; WX 600 ; N B ; B 30 0 630 562 ;
|
||||
C 67 ; WX 600 ; N C ; B 74 -18 675 580 ;
|
||||
C 68 ; WX 600 ; N D ; B 30 0 664 562 ;
|
||||
C 69 ; WX 600 ; N E ; B 25 0 670 562 ;
|
||||
C 70 ; WX 600 ; N F ; B 39 0 684 562 ;
|
||||
C 71 ; WX 600 ; N G ; B 74 -18 675 580 ;
|
||||
C 72 ; WX 600 ; N H ; B 20 0 700 562 ;
|
||||
C 73 ; WX 600 ; N I ; B 77 0 643 562 ;
|
||||
C 74 ; WX 600 ; N J ; B 58 -18 721 562 ;
|
||||
C 75 ; WX 600 ; N K ; B 21 0 692 562 ;
|
||||
C 76 ; WX 600 ; N L ; B 39 0 636 562 ;
|
||||
C 77 ; WX 600 ; N M ; B -2 0 722 562 ;
|
||||
C 78 ; WX 600 ; N N ; B 8 -12 730 562 ;
|
||||
C 79 ; WX 600 ; N O ; B 74 -18 645 580 ;
|
||||
C 80 ; WX 600 ; N P ; B 48 0 643 562 ;
|
||||
C 81 ; WX 600 ; N Q ; B 83 -138 636 580 ;
|
||||
C 82 ; WX 600 ; N R ; B 24 0 617 562 ;
|
||||
C 83 ; WX 600 ; N S ; B 54 -22 673 582 ;
|
||||
C 84 ; WX 600 ; N T ; B 86 0 679 562 ;
|
||||
C 85 ; WX 600 ; N U ; B 101 -18 716 562 ;
|
||||
C 86 ; WX 600 ; N V ; B 84 0 733 562 ;
|
||||
C 87 ; WX 600 ; N W ; B 79 0 738 562 ;
|
||||
C 88 ; WX 600 ; N X ; B 12 0 690 562 ;
|
||||
C 89 ; WX 600 ; N Y ; B 109 0 709 562 ;
|
||||
C 90 ; WX 600 ; N Z ; B 62 0 637 562 ;
|
||||
C 91 ; WX 600 ; N bracketleft ; B 223 -102 606 616 ;
|
||||
C 92 ; WX 600 ; N backslash ; B 222 -77 496 626 ;
|
||||
C 93 ; WX 600 ; N bracketright ; B 103 -102 486 616 ;
|
||||
C 94 ; WX 600 ; N asciicircum ; B 171 250 556 616 ;
|
||||
C 95 ; WX 600 ; N underscore ; B -27 -125 585 -75 ;
|
||||
C 145 ; WX 600 ; N quoteleft ; B 297 277 487 562 ;
|
||||
C 97 ; WX 600 ; N a ; B 61 -15 593 454 ;
|
||||
C 98 ; WX 600 ; N b ; B 13 -15 636 626 ;
|
||||
C 99 ; WX 600 ; N c ; B 81 -15 631 459 ;
|
||||
C 100 ; WX 600 ; N d ; B 60 -15 645 626 ;
|
||||
C 101 ; WX 600 ; N e ; B 81 -15 605 454 ;
|
||||
C 102 ; WX 600 ; N f ; B 83 0 677 626 ; L i fi ; L l fl ;
|
||||
C 103 ; WX 600 ; N g ; B 40 -146 674 454 ;
|
||||
C 104 ; WX 600 ; N h ; B 18 0 615 626 ;
|
||||
C 105 ; WX 600 ; N i ; B 77 0 546 658 ;
|
||||
C 106 ; WX 600 ; N j ; B 36 -146 580 658 ;
|
||||
C 107 ; WX 600 ; N k ; B 33 0 643 626 ;
|
||||
C 108 ; WX 600 ; N l ; B 77 0 546 626 ;
|
||||
C 109 ; WX 600 ; N m ; B -22 0 649 454 ;
|
||||
C 110 ; WX 600 ; N n ; B 18 0 615 454 ;
|
||||
C 111 ; WX 600 ; N o ; B 71 -15 622 454 ;
|
||||
C 112 ; WX 600 ; N p ; B -32 -142 622 454 ;
|
||||
C 113 ; WX 600 ; N q ; B 60 -142 685 454 ;
|
||||
C 114 ; WX 600 ; N r ; B 47 0 655 454 ;
|
||||
C 115 ; WX 600 ; N s ; B 66 -17 608 459 ;
|
||||
C 116 ; WX 600 ; N t ; B 118 -15 567 562 ;
|
||||
C 117 ; WX 600 ; N u ; B 70 -15 592 439 ;
|
||||
C 118 ; WX 600 ; N v ; B 70 0 695 439 ;
|
||||
C 119 ; WX 600 ; N w ; B 53 0 712 439 ;
|
||||
C 120 ; WX 600 ; N x ; B 6 0 671 439 ;
|
||||
C 121 ; WX 600 ; N y ; B -21 -142 695 439 ;
|
||||
C 122 ; WX 600 ; N z ; B 81 0 614 439 ;
|
||||
C 123 ; WX 600 ; N braceleft ; B 203 -102 595 616 ;
|
||||
C 124 ; WX 600 ; N bar ; B 201 -250 505 750 ;
|
||||
C 125 ; WX 600 ; N braceright ; B 114 -102 506 616 ;
|
||||
C 126 ; WX 600 ; N asciitilde ; B 120 153 590 356 ;
|
||||
C 161 ; WX 600 ; N exclamdown ; B 196 -146 477 449 ;
|
||||
C 162 ; WX 600 ; N cent ; B 121 -49 605 614 ;
|
||||
C 163 ; WX 600 ; N sterling ; B 106 -28 650 611 ;
|
||||
C -1 ; WX 600 ; N fraction ; B 22 -60 708 661 ;
|
||||
C 165 ; WX 600 ; N yen ; B 98 0 710 562 ;
|
||||
C 131 ; WX 600 ; N florin ; B -57 -131 702 616 ;
|
||||
C 167 ; WX 600 ; N section ; B 74 -70 620 580 ;
|
||||
C 164 ; WX 600 ; N currency ; B 77 49 644 517 ;
|
||||
C 39 ; WX 600 ; N quotesingle ; B 303 277 493 562 ;
|
||||
C 147 ; WX 600 ; N quotedblleft ; B 190 277 594 562 ;
|
||||
C 171 ; WX 600 ; N guillemotleft ; B 62 70 639 446 ;
|
||||
C 139 ; WX 600 ; N guilsinglleft ; B 195 70 545 446 ;
|
||||
C 155 ; WX 600 ; N guilsinglright ; B 165 70 514 446 ;
|
||||
C -1 ; WX 600 ; N fi ; B 12 0 644 626 ;
|
||||
C -1 ; WX 600 ; N fl ; B 12 0 644 626 ;
|
||||
C 150 ; WX 600 ; N endash ; B 108 203 602 313 ;
|
||||
C 134 ; WX 600 ; N dagger ; B 175 -70 586 580 ;
|
||||
C 135 ; WX 600 ; N daggerdbl ; B 121 -70 587 580 ;
|
||||
C 183 ; WX 600 ; N periodcentered ; B 248 165 461 351 ;
|
||||
C 182 ; WX 600 ; N paragraph ; B 61 -70 700 580 ;
|
||||
C 149 ; WX 600 ; N bullet ; B 196 132 523 430 ;
|
||||
C 130 ; WX 600 ; N quotesinglbase ; B 144 -142 458 143 ;
|
||||
C 132 ; WX 600 ; N quotedblbase ; B 34 -142 560 143 ;
|
||||
C 148 ; WX 600 ; N quotedblright ; B 119 277 645 562 ;
|
||||
C 187 ; WX 600 ; N guillemotright ; B 71 70 647 446 ;
|
||||
C 133 ; WX 600 ; N ellipsis ; B 35 -15 587 116 ;
|
||||
C 137 ; WX 600 ; N perthousand ; B -45 -15 743 616 ;
|
||||
C 191 ; WX 600 ; N questiondown ; B 100 -146 509 449 ;
|
||||
C 96 ; WX 600 ; N grave ; B 272 508 503 661 ;
|
||||
C 180 ; WX 600 ; N acute ; B 312 508 609 661 ;
|
||||
C 136 ; WX 600 ; N circumflex ; B 212 483 607 657 ;
|
||||
C 152 ; WX 600 ; N tilde ; B 199 493 643 636 ;
|
||||
C 175 ; WX 600 ; N macron ; B 195 505 637 585 ;
|
||||
C -1 ; WX 600 ; N breve ; B 217 468 652 631 ;
|
||||
C -1 ; WX 600 ; N dotaccent ; B 348 498 493 638 ;
|
||||
C 168 ; WX 600 ; N dieresis ; B 246 498 595 638 ;
|
||||
C -1 ; WX 600 ; N ring ; B 319 481 528 678 ;
|
||||
C 184 ; WX 600 ; N cedilla ; B 168 -206 368 0 ;
|
||||
C -1 ; WX 600 ; N hungarumlaut ; B 171 488 729 661 ;
|
||||
C -1 ; WX 600 ; N ogonek ; B 143 -199 367 0 ;
|
||||
C -1 ; WX 600 ; N caron ; B 238 493 633 667 ;
|
||||
C 151 ; WX 600 ; N emdash ; B 33 203 677 313 ;
|
||||
C 198 ; WX 600 ; N AE ; B -29 0 708 562 ;
|
||||
C 170 ; WX 600 ; N ordfeminine ; B 188 196 526 580 ;
|
||||
C -1 ; WX 600 ; N Lslash ; B 39 0 636 562 ;
|
||||
C 216 ; WX 600 ; N Oslash ; B 48 -22 673 584 ;
|
||||
C 140 ; WX 600 ; N OE ; B 26 0 701 562 ;
|
||||
C 186 ; WX 600 ; N ordmasculine ; B 188 196 543 580 ;
|
||||
C 230 ; WX 600 ; N ae ; B 21 -15 652 454 ;
|
||||
C -1 ; WX 600 ; N dotlessi ; B 77 0 546 439 ;
|
||||
C -1 ; WX 600 ; N lslash ; B 77 0 587 626 ;
|
||||
C 248 ; WX 600 ; N oslash ; B 54 -24 638 463 ;
|
||||
C 156 ; WX 600 ; N oe ; B 18 -15 662 454 ;
|
||||
C 223 ; WX 600 ; N germandbls ; B 22 -15 629 626 ;
|
||||
C 207 ; WX 600 ; N Idieresis ; B 77 0 643 761 ;
|
||||
C 233 ; WX 600 ; N eacute ; B 81 -15 609 661 ;
|
||||
C -1 ; WX 600 ; N abreve ; B 61 -15 658 661 ;
|
||||
C -1 ; WX 600 ; N uhungarumlaut ; B 70 -15 769 661 ;
|
||||
C -1 ; WX 600 ; N ecaron ; B 81 -15 633 667 ;
|
||||
C 159 ; WX 600 ; N Ydieresis ; B 109 0 709 761 ;
|
||||
C 247 ; WX 600 ; N divide ; B 114 16 596 500 ;
|
||||
C 221 ; WX 600 ; N Yacute ; B 109 0 709 784 ;
|
||||
C 194 ; WX 600 ; N Acircumflex ; B -9 0 632 780 ;
|
||||
C 225 ; WX 600 ; N aacute ; B 61 -15 609 661 ;
|
||||
C 219 ; WX 600 ; N Ucircumflex ; B 101 -18 716 780 ;
|
||||
C 253 ; WX 600 ; N yacute ; B -21 -142 695 661 ;
|
||||
C -1 ; WX 600 ; N scommaaccent ; B 66 -250 608 459 ;
|
||||
C 234 ; WX 600 ; N ecircumflex ; B 81 -15 607 657 ;
|
||||
C -1 ; WX 600 ; N Uring ; B 101 -18 716 801 ;
|
||||
C 220 ; WX 600 ; N Udieresis ; B 101 -18 716 761 ;
|
||||
C -1 ; WX 600 ; N aogonek ; B 61 -199 593 454 ;
|
||||
C 218 ; WX 600 ; N Uacute ; B 101 -18 716 784 ;
|
||||
C -1 ; WX 600 ; N uogonek ; B 70 -199 592 439 ;
|
||||
C 203 ; WX 600 ; N Edieresis ; B 25 0 670 761 ;
|
||||
C -1 ; WX 600 ; N Dcroat ; B 30 0 664 562 ;
|
||||
C -1 ; WX 600 ; N commaaccent ; B 151 -250 385 -57 ;
|
||||
C 169 ; WX 600 ; N copyright ; B 53 -18 667 580 ;
|
||||
C -1 ; WX 600 ; N Emacron ; B 25 0 670 708 ;
|
||||
C -1 ; WX 600 ; N ccaron ; B 81 -15 633 667 ;
|
||||
C 229 ; WX 600 ; N aring ; B 61 -15 593 678 ;
|
||||
C -1 ; WX 600 ; N Ncommaaccent ; B 8 -250 730 562 ;
|
||||
C -1 ; WX 600 ; N lacute ; B 77 0 639 801 ;
|
||||
C 224 ; WX 600 ; N agrave ; B 61 -15 593 661 ;
|
||||
C -1 ; WX 600 ; N Tcommaaccent ; B 86 -250 679 562 ;
|
||||
C -1 ; WX 600 ; N Cacute ; B 74 -18 675 784 ;
|
||||
C 227 ; WX 600 ; N atilde ; B 61 -15 643 636 ;
|
||||
C -1 ; WX 600 ; N Edotaccent ; B 25 0 670 761 ;
|
||||
C 154 ; WX 600 ; N scaron ; B 66 -17 633 667 ;
|
||||
C -1 ; WX 600 ; N scedilla ; B 66 -206 608 459 ;
|
||||
C 237 ; WX 600 ; N iacute ; B 77 0 609 661 ;
|
||||
C -1 ; WX 600 ; N lozenge ; B 145 0 614 740 ;
|
||||
C -1 ; WX 600 ; N Rcaron ; B 24 0 659 790 ;
|
||||
C -1 ; WX 600 ; N Gcommaaccent ; B 74 -250 675 580 ;
|
||||
C 251 ; WX 600 ; N ucircumflex ; B 70 -15 597 657 ;
|
||||
C 226 ; WX 600 ; N acircumflex ; B 61 -15 607 657 ;
|
||||
C -1 ; WX 600 ; N Amacron ; B -9 0 633 708 ;
|
||||
C -1 ; WX 600 ; N rcaron ; B 47 0 655 667 ;
|
||||
C 231 ; WX 600 ; N ccedilla ; B 81 -206 631 459 ;
|
||||
C -1 ; WX 600 ; N Zdotaccent ; B 62 0 637 761 ;
|
||||
C 222 ; WX 600 ; N Thorn ; B 48 0 620 562 ;
|
||||
C -1 ; WX 600 ; N Omacron ; B 74 -18 663 708 ;
|
||||
C -1 ; WX 600 ; N Racute ; B 24 0 665 784 ;
|
||||
C -1 ; WX 600 ; N Sacute ; B 54 -22 673 784 ;
|
||||
C -1 ; WX 600 ; N dcaron ; B 60 -15 861 626 ;
|
||||
C -1 ; WX 600 ; N Umacron ; B 101 -18 716 708 ;
|
||||
C -1 ; WX 600 ; N uring ; B 70 -15 592 678 ;
|
||||
C 179 ; WX 600 ; N threesuperior ; B 193 222 526 616 ;
|
||||
C 210 ; WX 600 ; N Ograve ; B 74 -18 645 784 ;
|
||||
C 192 ; WX 600 ; N Agrave ; B -9 0 632 784 ;
|
||||
C -1 ; WX 600 ; N Abreve ; B -9 0 684 784 ;
|
||||
C 215 ; WX 600 ; N multiply ; B 104 39 606 478 ;
|
||||
C 250 ; WX 600 ; N uacute ; B 70 -15 599 661 ;
|
||||
C -1 ; WX 600 ; N Tcaron ; B 86 0 679 790 ;
|
||||
C -1 ; WX 600 ; N partialdiff ; B 91 -38 627 728 ;
|
||||
C 255 ; WX 600 ; N ydieresis ; B -21 -142 695 638 ;
|
||||
C -1 ; WX 600 ; N Nacute ; B 8 -12 730 784 ;
|
||||
C 238 ; WX 600 ; N icircumflex ; B 77 0 577 657 ;
|
||||
C 202 ; WX 600 ; N Ecircumflex ; B 25 0 670 780 ;
|
||||
C 228 ; WX 600 ; N adieresis ; B 61 -15 595 638 ;
|
||||
C 235 ; WX 600 ; N edieresis ; B 81 -15 605 638 ;
|
||||
C -1 ; WX 600 ; N cacute ; B 81 -15 649 661 ;
|
||||
C -1 ; WX 600 ; N nacute ; B 18 0 639 661 ;
|
||||
C -1 ; WX 600 ; N umacron ; B 70 -15 637 585 ;
|
||||
C -1 ; WX 600 ; N Ncaron ; B 8 -12 730 790 ;
|
||||
C 205 ; WX 600 ; N Iacute ; B 77 0 643 784 ;
|
||||
C 177 ; WX 600 ; N plusminus ; B 76 24 614 515 ;
|
||||
C 166 ; WX 600 ; N brokenbar ; B 217 -175 489 675 ;
|
||||
C 174 ; WX 600 ; N registered ; B 53 -18 667 580 ;
|
||||
C -1 ; WX 600 ; N Gbreve ; B 74 -18 684 784 ;
|
||||
C -1 ; WX 600 ; N Idotaccent ; B 77 0 643 761 ;
|
||||
C -1 ; WX 600 ; N summation ; B 15 -10 672 706 ;
|
||||
C 200 ; WX 600 ; N Egrave ; B 25 0 670 784 ;
|
||||
C -1 ; WX 600 ; N racute ; B 47 0 655 661 ;
|
||||
C -1 ; WX 600 ; N omacron ; B 71 -15 637 585 ;
|
||||
C -1 ; WX 600 ; N Zacute ; B 62 0 665 784 ;
|
||||
C 142 ; WX 600 ; N Zcaron ; B 62 0 659 790 ;
|
||||
C -1 ; WX 600 ; N greaterequal ; B 26 0 627 696 ;
|
||||
C 208 ; WX 600 ; N Eth ; B 30 0 664 562 ;
|
||||
C 199 ; WX 600 ; N Ccedilla ; B 74 -206 675 580 ;
|
||||
C -1 ; WX 600 ; N lcommaaccent ; B 77 -250 546 626 ;
|
||||
C -1 ; WX 600 ; N tcaron ; B 118 -15 627 703 ;
|
||||
C -1 ; WX 600 ; N eogonek ; B 81 -199 605 454 ;
|
||||
C -1 ; WX 600 ; N Uogonek ; B 101 -199 716 562 ;
|
||||
C 193 ; WX 600 ; N Aacute ; B -9 0 655 784 ;
|
||||
C 196 ; WX 600 ; N Adieresis ; B -9 0 632 761 ;
|
||||
C 232 ; WX 600 ; N egrave ; B 81 -15 605 661 ;
|
||||
C -1 ; WX 600 ; N zacute ; B 81 0 614 661 ;
|
||||
C -1 ; WX 600 ; N iogonek ; B 77 -199 546 658 ;
|
||||
C 211 ; WX 600 ; N Oacute ; B 74 -18 645 784 ;
|
||||
C 243 ; WX 600 ; N oacute ; B 71 -15 649 661 ;
|
||||
C -1 ; WX 600 ; N amacron ; B 61 -15 637 585 ;
|
||||
C -1 ; WX 600 ; N sacute ; B 66 -17 609 661 ;
|
||||
C 239 ; WX 600 ; N idieresis ; B 77 0 561 618 ;
|
||||
C 212 ; WX 600 ; N Ocircumflex ; B 74 -18 645 780 ;
|
||||
C 217 ; WX 600 ; N Ugrave ; B 101 -18 716 784 ;
|
||||
C -1 ; WX 600 ; N Delta ; B 6 0 594 688 ;
|
||||
C 254 ; WX 600 ; N thorn ; B -32 -142 622 626 ;
|
||||
C 178 ; WX 600 ; N twosuperior ; B 191 230 542 616 ;
|
||||
C 214 ; WX 600 ; N Odieresis ; B 74 -18 645 761 ;
|
||||
C 181 ; WX 600 ; N mu ; B 49 -142 592 439 ;
|
||||
C 236 ; WX 600 ; N igrave ; B 77 0 546 661 ;
|
||||
C -1 ; WX 600 ; N ohungarumlaut ; B 71 -15 809 661 ;
|
||||
C -1 ; WX 600 ; N Eogonek ; B 25 -199 670 562 ;
|
||||
C -1 ; WX 600 ; N dcroat ; B 60 -15 712 626 ;
|
||||
C 190 ; WX 600 ; N threequarters ; B 8 -60 699 661 ;
|
||||
C -1 ; WX 600 ; N Scedilla ; B 54 -206 673 582 ;
|
||||
C -1 ; WX 600 ; N lcaron ; B 77 0 731 626 ;
|
||||
C -1 ; WX 600 ; N Kcommaaccent ; B 21 -250 692 562 ;
|
||||
C -1 ; WX 600 ; N Lacute ; B 39 0 636 784 ;
|
||||
C 153 ; WX 600 ; N trademark ; B 86 230 869 562 ;
|
||||
C -1 ; WX 600 ; N edotaccent ; B 81 -15 605 638 ;
|
||||
C 204 ; WX 600 ; N Igrave ; B 77 0 643 784 ;
|
||||
C -1 ; WX 600 ; N Imacron ; B 77 0 663 708 ;
|
||||
C -1 ; WX 600 ; N Lcaron ; B 39 0 757 562 ;
|
||||
C 189 ; WX 600 ; N onehalf ; B 22 -60 716 661 ;
|
||||
C -1 ; WX 600 ; N lessequal ; B 26 0 671 696 ;
|
||||
C 244 ; WX 600 ; N ocircumflex ; B 71 -15 622 657 ;
|
||||
C 241 ; WX 600 ; N ntilde ; B 18 0 643 636 ;
|
||||
C -1 ; WX 600 ; N Uhungarumlaut ; B 101 -18 805 784 ;
|
||||
C 201 ; WX 600 ; N Eacute ; B 25 0 670 784 ;
|
||||
C -1 ; WX 600 ; N emacron ; B 81 -15 637 585 ;
|
||||
C -1 ; WX 600 ; N gbreve ; B 40 -146 674 661 ;
|
||||
C 188 ; WX 600 ; N onequarter ; B 13 -60 707 661 ;
|
||||
C 138 ; WX 600 ; N Scaron ; B 54 -22 689 790 ;
|
||||
C -1 ; WX 600 ; N Scommaaccent ; B 54 -250 673 582 ;
|
||||
C -1 ; WX 600 ; N Ohungarumlaut ; B 74 -18 795 784 ;
|
||||
C 176 ; WX 600 ; N degree ; B 173 243 570 616 ;
|
||||
C 242 ; WX 600 ; N ograve ; B 71 -15 622 661 ;
|
||||
C -1 ; WX 600 ; N Ccaron ; B 74 -18 689 790 ;
|
||||
C 249 ; WX 600 ; N ugrave ; B 70 -15 592 661 ;
|
||||
C -1 ; WX 600 ; N radical ; B 67 -104 635 778 ;
|
||||
C -1 ; WX 600 ; N Dcaron ; B 30 0 664 790 ;
|
||||
C -1 ; WX 600 ; N rcommaaccent ; B 47 -250 655 454 ;
|
||||
C 209 ; WX 600 ; N Ntilde ; B 8 -12 730 759 ;
|
||||
C 245 ; WX 600 ; N otilde ; B 71 -15 643 636 ;
|
||||
C -1 ; WX 600 ; N Rcommaaccent ; B 24 -250 617 562 ;
|
||||
C -1 ; WX 600 ; N Lcommaaccent ; B 39 -250 636 562 ;
|
||||
C 195 ; WX 600 ; N Atilde ; B -9 0 669 759 ;
|
||||
C -1 ; WX 600 ; N Aogonek ; B -9 -199 632 562 ;
|
||||
C 197 ; WX 600 ; N Aring ; B -9 0 632 801 ;
|
||||
C 213 ; WX 600 ; N Otilde ; B 74 -18 669 759 ;
|
||||
C -1 ; WX 600 ; N zdotaccent ; B 81 0 614 638 ;
|
||||
C -1 ; WX 600 ; N Ecaron ; B 25 0 670 790 ;
|
||||
C -1 ; WX 600 ; N Iogonek ; B 77 -199 643 562 ;
|
||||
C -1 ; WX 600 ; N kcommaaccent ; B 33 -250 643 626 ;
|
||||
C -1 ; WX 600 ; N minus ; B 114 203 596 313 ;
|
||||
C 206 ; WX 600 ; N Icircumflex ; B 77 0 643 780 ;
|
||||
C -1 ; WX 600 ; N ncaron ; B 18 0 633 667 ;
|
||||
C -1 ; WX 600 ; N tcommaaccent ; B 118 -250 567 562 ;
|
||||
C 172 ; WX 600 ; N logicalnot ; B 135 103 617 413 ;
|
||||
C 246 ; WX 600 ; N odieresis ; B 71 -15 622 638 ;
|
||||
C 252 ; WX 600 ; N udieresis ; B 70 -15 595 638 ;
|
||||
C -1 ; WX 600 ; N notequal ; B 30 -47 626 563 ;
|
||||
C -1 ; WX 600 ; N gcommaaccent ; B 40 -146 674 714 ;
|
||||
C 240 ; WX 600 ; N eth ; B 93 -27 661 626 ;
|
||||
C 158 ; WX 600 ; N zcaron ; B 81 0 643 667 ;
|
||||
C -1 ; WX 600 ; N ncommaaccent ; B 18 -250 615 454 ;
|
||||
C 185 ; WX 600 ; N onesuperior ; B 212 230 514 616 ;
|
||||
C -1 ; WX 600 ; N imacron ; B 77 0 575 585 ;
|
||||
C 128 ; WX 600 ; N Euro ; B 0 0 0 0 ;
|
||||
EndCharMetrics
|
||||
EndFontMetrics
|
||||
344
pirp/vendor/dompdf/dompdf/lib/fonts/Courier-Oblique.afm
vendored
Normal file
344
pirp/vendor/dompdf/dompdf/lib/fonts/Courier-Oblique.afm
vendored
Normal file
@@ -0,0 +1,344 @@
|
||||
StartFontMetrics 4.1
|
||||
Comment Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved.
|
||||
Comment Creation Date: Thu May 0:00:00 17:37:52 1997
|
||||
Comment UniqueID 43051
|
||||
Comment VMusage 16248 75829
|
||||
FontName Courier-Oblique
|
||||
FullName Courier Oblique
|
||||
FamilyName Courier
|
||||
Weight Medium
|
||||
ItalicAngle -12
|
||||
IsFixedPitch true
|
||||
CharacterSet ExtendedRoman
|
||||
FontBBox -27 -250 849 805
|
||||
UnderlinePosition -100
|
||||
UnderlineThickness 50
|
||||
Version 003.000
|
||||
Notice Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved.
|
||||
EncodingScheme WinAnsiEncoding
|
||||
CapHeight 562
|
||||
XHeight 426
|
||||
Ascender 629
|
||||
Descender -157
|
||||
StdHW 51
|
||||
StdVW 51
|
||||
StartCharMetrics 317
|
||||
C 32 ; WX 600 ; N space ; B 0 0 0 0 ;
|
||||
C 160 ; WX 600 ; N space ; B 0 0 0 0 ;
|
||||
C 33 ; WX 600 ; N exclam ; B 243 -15 464 572 ;
|
||||
C 34 ; WX 600 ; N quotedbl ; B 273 328 532 562 ;
|
||||
C 35 ; WX 600 ; N numbersign ; B 133 -32 596 639 ;
|
||||
C 36 ; WX 600 ; N dollar ; B 108 -126 596 662 ;
|
||||
C 37 ; WX 600 ; N percent ; B 134 -15 599 622 ;
|
||||
C 38 ; WX 600 ; N ampersand ; B 87 -15 580 543 ;
|
||||
C 146 ; WX 600 ; N quoteright ; B 283 328 495 562 ;
|
||||
C 40 ; WX 600 ; N parenleft ; B 313 -108 572 622 ;
|
||||
C 41 ; WX 600 ; N parenright ; B 137 -108 396 622 ;
|
||||
C 42 ; WX 600 ; N asterisk ; B 212 257 580 607 ;
|
||||
C 43 ; WX 600 ; N plus ; B 129 44 580 470 ;
|
||||
C 44 ; WX 600 ; N comma ; B 157 -112 370 122 ;
|
||||
C 45 ; WX 600 ; N hyphen ; B 152 231 558 285 ;
|
||||
C 173 ; WX 600 ; N hyphen ; B 152 231 558 285 ;
|
||||
C 46 ; WX 600 ; N period ; B 238 -15 382 109 ;
|
||||
C 47 ; WX 600 ; N slash ; B 112 -80 604 629 ;
|
||||
C 48 ; WX 600 ; N zero ; B 154 -15 575 622 ;
|
||||
C 49 ; WX 600 ; N one ; B 98 0 515 622 ;
|
||||
C 50 ; WX 600 ; N two ; B 70 0 568 622 ;
|
||||
C 51 ; WX 600 ; N three ; B 82 -15 538 622 ;
|
||||
C 52 ; WX 600 ; N four ; B 108 0 541 622 ;
|
||||
C 53 ; WX 600 ; N five ; B 99 -15 589 607 ;
|
||||
C 54 ; WX 600 ; N six ; B 155 -15 629 622 ;
|
||||
C 55 ; WX 600 ; N seven ; B 182 0 612 607 ;
|
||||
C 56 ; WX 600 ; N eight ; B 132 -15 588 622 ;
|
||||
C 57 ; WX 600 ; N nine ; B 93 -15 574 622 ;
|
||||
C 58 ; WX 600 ; N colon ; B 238 -15 441 385 ;
|
||||
C 59 ; WX 600 ; N semicolon ; B 157 -112 441 385 ;
|
||||
C 60 ; WX 600 ; N less ; B 96 42 610 472 ;
|
||||
C 61 ; WX 600 ; N equal ; B 109 138 600 376 ;
|
||||
C 62 ; WX 600 ; N greater ; B 85 42 599 472 ;
|
||||
C 63 ; WX 600 ; N question ; B 222 -15 583 572 ;
|
||||
C 64 ; WX 600 ; N at ; B 127 -15 582 622 ;
|
||||
C 65 ; WX 600 ; N A ; B 3 0 607 562 ;
|
||||
C 66 ; WX 600 ; N B ; B 43 0 616 562 ;
|
||||
C 67 ; WX 600 ; N C ; B 93 -18 655 580 ;
|
||||
C 68 ; WX 600 ; N D ; B 43 0 645 562 ;
|
||||
C 69 ; WX 600 ; N E ; B 53 0 660 562 ;
|
||||
C 70 ; WX 600 ; N F ; B 53 0 660 562 ;
|
||||
C 71 ; WX 600 ; N G ; B 83 -18 645 580 ;
|
||||
C 72 ; WX 600 ; N H ; B 32 0 687 562 ;
|
||||
C 73 ; WX 600 ; N I ; B 96 0 623 562 ;
|
||||
C 74 ; WX 600 ; N J ; B 52 -18 685 562 ;
|
||||
C 75 ; WX 600 ; N K ; B 38 0 671 562 ;
|
||||
C 76 ; WX 600 ; N L ; B 47 0 607 562 ;
|
||||
C 77 ; WX 600 ; N M ; B 4 0 715 562 ;
|
||||
C 78 ; WX 600 ; N N ; B 7 -13 712 562 ;
|
||||
C 79 ; WX 600 ; N O ; B 94 -18 625 580 ;
|
||||
C 80 ; WX 600 ; N P ; B 79 0 644 562 ;
|
||||
C 81 ; WX 600 ; N Q ; B 95 -138 625 580 ;
|
||||
C 82 ; WX 600 ; N R ; B 38 0 598 562 ;
|
||||
C 83 ; WX 600 ; N S ; B 76 -20 650 580 ;
|
||||
C 84 ; WX 600 ; N T ; B 108 0 665 562 ;
|
||||
C 85 ; WX 600 ; N U ; B 125 -18 702 562 ;
|
||||
C 86 ; WX 600 ; N V ; B 105 -13 723 562 ;
|
||||
C 87 ; WX 600 ; N W ; B 106 -13 722 562 ;
|
||||
C 88 ; WX 600 ; N X ; B 23 0 675 562 ;
|
||||
C 89 ; WX 600 ; N Y ; B 133 0 695 562 ;
|
||||
C 90 ; WX 600 ; N Z ; B 86 0 610 562 ;
|
||||
C 91 ; WX 600 ; N bracketleft ; B 246 -108 574 622 ;
|
||||
C 92 ; WX 600 ; N backslash ; B 249 -80 468 629 ;
|
||||
C 93 ; WX 600 ; N bracketright ; B 135 -108 463 622 ;
|
||||
C 94 ; WX 600 ; N asciicircum ; B 175 354 587 622 ;
|
||||
C 95 ; WX 600 ; N underscore ; B -27 -125 584 -75 ;
|
||||
C 145 ; WX 600 ; N quoteleft ; B 343 328 457 562 ;
|
||||
C 97 ; WX 600 ; N a ; B 76 -15 569 441 ;
|
||||
C 98 ; WX 600 ; N b ; B 29 -15 625 629 ;
|
||||
C 99 ; WX 600 ; N c ; B 106 -15 608 441 ;
|
||||
C 100 ; WX 600 ; N d ; B 85 -15 640 629 ;
|
||||
C 101 ; WX 600 ; N e ; B 106 -15 598 441 ;
|
||||
C 102 ; WX 600 ; N f ; B 114 0 662 629 ; L i fi ; L l fl ;
|
||||
C 103 ; WX 600 ; N g ; B 61 -157 657 441 ;
|
||||
C 104 ; WX 600 ; N h ; B 33 0 592 629 ;
|
||||
C 105 ; WX 600 ; N i ; B 95 0 515 657 ;
|
||||
C 106 ; WX 600 ; N j ; B 52 -157 550 657 ;
|
||||
C 107 ; WX 600 ; N k ; B 58 0 633 629 ;
|
||||
C 108 ; WX 600 ; N l ; B 95 0 515 629 ;
|
||||
C 109 ; WX 600 ; N m ; B -5 0 615 441 ;
|
||||
C 110 ; WX 600 ; N n ; B 26 0 585 441 ;
|
||||
C 111 ; WX 600 ; N o ; B 102 -15 588 441 ;
|
||||
C 112 ; WX 600 ; N p ; B -24 -157 605 441 ;
|
||||
C 113 ; WX 600 ; N q ; B 85 -157 682 441 ;
|
||||
C 114 ; WX 600 ; N r ; B 60 0 636 441 ;
|
||||
C 115 ; WX 600 ; N s ; B 78 -15 584 441 ;
|
||||
C 116 ; WX 600 ; N t ; B 167 -15 561 561 ;
|
||||
C 117 ; WX 600 ; N u ; B 101 -15 572 426 ;
|
||||
C 118 ; WX 600 ; N v ; B 90 -10 681 426 ;
|
||||
C 119 ; WX 600 ; N w ; B 76 -10 695 426 ;
|
||||
C 120 ; WX 600 ; N x ; B 20 0 655 426 ;
|
||||
C 121 ; WX 600 ; N y ; B -4 -157 683 426 ;
|
||||
C 122 ; WX 600 ; N z ; B 99 0 593 426 ;
|
||||
C 123 ; WX 600 ; N braceleft ; B 233 -108 569 622 ;
|
||||
C 124 ; WX 600 ; N bar ; B 222 -250 485 750 ;
|
||||
C 125 ; WX 600 ; N braceright ; B 140 -108 477 622 ;
|
||||
C 126 ; WX 600 ; N asciitilde ; B 116 197 600 320 ;
|
||||
C 161 ; WX 600 ; N exclamdown ; B 225 -157 445 430 ;
|
||||
C 162 ; WX 600 ; N cent ; B 151 -49 588 614 ;
|
||||
C 163 ; WX 600 ; N sterling ; B 124 -21 621 611 ;
|
||||
C -1 ; WX 600 ; N fraction ; B 84 -57 646 665 ;
|
||||
C 165 ; WX 600 ; N yen ; B 120 0 693 562 ;
|
||||
C 131 ; WX 600 ; N florin ; B -26 -143 671 622 ;
|
||||
C 167 ; WX 600 ; N section ; B 104 -78 590 580 ;
|
||||
C 164 ; WX 600 ; N currency ; B 94 58 628 506 ;
|
||||
C 39 ; WX 600 ; N quotesingle ; B 345 328 460 562 ;
|
||||
C 147 ; WX 600 ; N quotedblleft ; B 262 328 541 562 ;
|
||||
C 171 ; WX 600 ; N guillemotleft ; B 92 70 652 446 ;
|
||||
C 139 ; WX 600 ; N guilsinglleft ; B 204 70 540 446 ;
|
||||
C 155 ; WX 600 ; N guilsinglright ; B 170 70 506 446 ;
|
||||
C -1 ; WX 600 ; N fi ; B 3 0 619 629 ;
|
||||
C -1 ; WX 600 ; N fl ; B 3 0 619 629 ;
|
||||
C 150 ; WX 600 ; N endash ; B 124 231 586 285 ;
|
||||
C 134 ; WX 600 ; N dagger ; B 217 -78 546 580 ;
|
||||
C 135 ; WX 600 ; N daggerdbl ; B 163 -78 546 580 ;
|
||||
C 183 ; WX 600 ; N periodcentered ; B 275 189 434 327 ;
|
||||
C 182 ; WX 600 ; N paragraph ; B 100 -78 630 562 ;
|
||||
C 149 ; WX 600 ; N bullet ; B 224 130 485 383 ;
|
||||
C 130 ; WX 600 ; N quotesinglbase ; B 185 -134 397 100 ;
|
||||
C 132 ; WX 600 ; N quotedblbase ; B 115 -134 478 100 ;
|
||||
C 148 ; WX 600 ; N quotedblright ; B 213 328 576 562 ;
|
||||
C 187 ; WX 600 ; N guillemotright ; B 58 70 618 446 ;
|
||||
C 133 ; WX 600 ; N ellipsis ; B 46 -15 575 111 ;
|
||||
C 137 ; WX 600 ; N perthousand ; B 59 -15 627 622 ;
|
||||
C 191 ; WX 600 ; N questiondown ; B 105 -157 466 430 ;
|
||||
C 96 ; WX 600 ; N grave ; B 294 497 484 672 ;
|
||||
C 180 ; WX 600 ; N acute ; B 348 497 612 672 ;
|
||||
C 136 ; WX 600 ; N circumflex ; B 229 477 581 654 ;
|
||||
C 152 ; WX 600 ; N tilde ; B 212 489 629 606 ;
|
||||
C 175 ; WX 600 ; N macron ; B 232 525 600 565 ;
|
||||
C -1 ; WX 600 ; N breve ; B 279 501 576 609 ;
|
||||
C -1 ; WX 600 ; N dotaccent ; B 373 537 478 640 ;
|
||||
C 168 ; WX 600 ; N dieresis ; B 272 537 579 640 ;
|
||||
C -1 ; WX 600 ; N ring ; B 332 463 500 627 ;
|
||||
C 184 ; WX 600 ; N cedilla ; B 197 -151 344 10 ;
|
||||
C -1 ; WX 600 ; N hungarumlaut ; B 239 497 683 672 ;
|
||||
C -1 ; WX 600 ; N ogonek ; B 189 -172 377 4 ;
|
||||
C -1 ; WX 600 ; N caron ; B 262 492 614 669 ;
|
||||
C 151 ; WX 600 ; N emdash ; B 49 231 661 285 ;
|
||||
C 198 ; WX 600 ; N AE ; B 3 0 655 562 ;
|
||||
C 170 ; WX 600 ; N ordfeminine ; B 209 249 512 580 ;
|
||||
C -1 ; WX 600 ; N Lslash ; B 47 0 607 562 ;
|
||||
C 216 ; WX 600 ; N Oslash ; B 94 -80 625 629 ;
|
||||
C 140 ; WX 600 ; N OE ; B 59 0 672 562 ;
|
||||
C 186 ; WX 600 ; N ordmasculine ; B 210 249 535 580 ;
|
||||
C 230 ; WX 600 ; N ae ; B 41 -15 626 441 ;
|
||||
C -1 ; WX 600 ; N dotlessi ; B 95 0 515 426 ;
|
||||
C -1 ; WX 600 ; N lslash ; B 95 0 587 629 ;
|
||||
C 248 ; WX 600 ; N oslash ; B 102 -80 588 506 ;
|
||||
C 156 ; WX 600 ; N oe ; B 54 -15 615 441 ;
|
||||
C 223 ; WX 600 ; N germandbls ; B 48 -15 617 629 ;
|
||||
C 207 ; WX 600 ; N Idieresis ; B 96 0 623 753 ;
|
||||
C 233 ; WX 600 ; N eacute ; B 106 -15 612 672 ;
|
||||
C -1 ; WX 600 ; N abreve ; B 76 -15 576 609 ;
|
||||
C -1 ; WX 600 ; N uhungarumlaut ; B 101 -15 723 672 ;
|
||||
C -1 ; WX 600 ; N ecaron ; B 106 -15 614 669 ;
|
||||
C 159 ; WX 600 ; N Ydieresis ; B 133 0 695 753 ;
|
||||
C 247 ; WX 600 ; N divide ; B 136 48 573 467 ;
|
||||
C 221 ; WX 600 ; N Yacute ; B 133 0 695 805 ;
|
||||
C 194 ; WX 600 ; N Acircumflex ; B 3 0 607 787 ;
|
||||
C 225 ; WX 600 ; N aacute ; B 76 -15 612 672 ;
|
||||
C 219 ; WX 600 ; N Ucircumflex ; B 125 -18 702 787 ;
|
||||
C 253 ; WX 600 ; N yacute ; B -4 -157 683 672 ;
|
||||
C -1 ; WX 600 ; N scommaaccent ; B 78 -250 584 441 ;
|
||||
C 234 ; WX 600 ; N ecircumflex ; B 106 -15 598 654 ;
|
||||
C -1 ; WX 600 ; N Uring ; B 125 -18 702 760 ;
|
||||
C 220 ; WX 600 ; N Udieresis ; B 125 -18 702 753 ;
|
||||
C -1 ; WX 600 ; N aogonek ; B 76 -172 569 441 ;
|
||||
C 218 ; WX 600 ; N Uacute ; B 125 -18 702 805 ;
|
||||
C -1 ; WX 600 ; N uogonek ; B 101 -172 572 426 ;
|
||||
C 203 ; WX 600 ; N Edieresis ; B 53 0 660 753 ;
|
||||
C -1 ; WX 600 ; N Dcroat ; B 43 0 645 562 ;
|
||||
C -1 ; WX 600 ; N commaaccent ; B 145 -250 323 -58 ;
|
||||
C 169 ; WX 600 ; N copyright ; B 53 -18 667 580 ;
|
||||
C -1 ; WX 600 ; N Emacron ; B 53 0 660 698 ;
|
||||
C -1 ; WX 600 ; N ccaron ; B 106 -15 614 669 ;
|
||||
C 229 ; WX 600 ; N aring ; B 76 -15 569 627 ;
|
||||
C -1 ; WX 600 ; N Ncommaaccent ; B 7 -250 712 562 ;
|
||||
C -1 ; WX 600 ; N lacute ; B 95 0 640 805 ;
|
||||
C 224 ; WX 600 ; N agrave ; B 76 -15 569 672 ;
|
||||
C -1 ; WX 600 ; N Tcommaaccent ; B 108 -250 665 562 ;
|
||||
C -1 ; WX 600 ; N Cacute ; B 93 -18 655 805 ;
|
||||
C 227 ; WX 600 ; N atilde ; B 76 -15 629 606 ;
|
||||
C -1 ; WX 600 ; N Edotaccent ; B 53 0 660 753 ;
|
||||
C 154 ; WX 600 ; N scaron ; B 78 -15 614 669 ;
|
||||
C -1 ; WX 600 ; N scedilla ; B 78 -151 584 441 ;
|
||||
C 237 ; WX 600 ; N iacute ; B 95 0 612 672 ;
|
||||
C -1 ; WX 600 ; N lozenge ; B 94 0 519 706 ;
|
||||
C -1 ; WX 600 ; N Rcaron ; B 38 0 642 802 ;
|
||||
C -1 ; WX 600 ; N Gcommaaccent ; B 83 -250 645 580 ;
|
||||
C 251 ; WX 600 ; N ucircumflex ; B 101 -15 572 654 ;
|
||||
C 226 ; WX 600 ; N acircumflex ; B 76 -15 581 654 ;
|
||||
C -1 ; WX 600 ; N Amacron ; B 3 0 607 698 ;
|
||||
C -1 ; WX 600 ; N rcaron ; B 60 0 636 669 ;
|
||||
C 231 ; WX 600 ; N ccedilla ; B 106 -151 614 441 ;
|
||||
C -1 ; WX 600 ; N Zdotaccent ; B 86 0 610 753 ;
|
||||
C 222 ; WX 600 ; N Thorn ; B 79 0 606 562 ;
|
||||
C -1 ; WX 600 ; N Omacron ; B 94 -18 628 698 ;
|
||||
C -1 ; WX 600 ; N Racute ; B 38 0 670 805 ;
|
||||
C -1 ; WX 600 ; N Sacute ; B 76 -20 650 805 ;
|
||||
C -1 ; WX 600 ; N dcaron ; B 85 -15 849 629 ;
|
||||
C -1 ; WX 600 ; N Umacron ; B 125 -18 702 698 ;
|
||||
C -1 ; WX 600 ; N uring ; B 101 -15 572 627 ;
|
||||
C 179 ; WX 600 ; N threesuperior ; B 213 240 501 622 ;
|
||||
C 210 ; WX 600 ; N Ograve ; B 94 -18 625 805 ;
|
||||
C 192 ; WX 600 ; N Agrave ; B 3 0 607 805 ;
|
||||
C -1 ; WX 600 ; N Abreve ; B 3 0 607 732 ;
|
||||
C 215 ; WX 600 ; N multiply ; B 103 43 607 470 ;
|
||||
C 250 ; WX 600 ; N uacute ; B 101 -15 602 672 ;
|
||||
C -1 ; WX 600 ; N Tcaron ; B 108 0 665 802 ;
|
||||
C -1 ; WX 600 ; N partialdiff ; B 45 -38 546 710 ;
|
||||
C 255 ; WX 600 ; N ydieresis ; B -4 -157 683 620 ;
|
||||
C -1 ; WX 600 ; N Nacute ; B 7 -13 712 805 ;
|
||||
C 238 ; WX 600 ; N icircumflex ; B 95 0 551 654 ;
|
||||
C 202 ; WX 600 ; N Ecircumflex ; B 53 0 660 787 ;
|
||||
C 228 ; WX 600 ; N adieresis ; B 76 -15 575 620 ;
|
||||
C 235 ; WX 600 ; N edieresis ; B 106 -15 598 620 ;
|
||||
C -1 ; WX 600 ; N cacute ; B 106 -15 612 672 ;
|
||||
C -1 ; WX 600 ; N nacute ; B 26 0 602 672 ;
|
||||
C -1 ; WX 600 ; N umacron ; B 101 -15 600 565 ;
|
||||
C -1 ; WX 600 ; N Ncaron ; B 7 -13 712 802 ;
|
||||
C 205 ; WX 600 ; N Iacute ; B 96 0 640 805 ;
|
||||
C 177 ; WX 600 ; N plusminus ; B 96 44 594 558 ;
|
||||
C 166 ; WX 600 ; N brokenbar ; B 238 -175 469 675 ;
|
||||
C 174 ; WX 600 ; N registered ; B 53 -18 667 580 ;
|
||||
C -1 ; WX 600 ; N Gbreve ; B 83 -18 645 732 ;
|
||||
C -1 ; WX 600 ; N Idotaccent ; B 96 0 623 753 ;
|
||||
C -1 ; WX 600 ; N summation ; B 15 -10 670 706 ;
|
||||
C 200 ; WX 600 ; N Egrave ; B 53 0 660 805 ;
|
||||
C -1 ; WX 600 ; N racute ; B 60 0 636 672 ;
|
||||
C -1 ; WX 600 ; N omacron ; B 102 -15 600 565 ;
|
||||
C -1 ; WX 600 ; N Zacute ; B 86 0 670 805 ;
|
||||
C 142 ; WX 600 ; N Zcaron ; B 86 0 642 802 ;
|
||||
C -1 ; WX 600 ; N greaterequal ; B 98 0 594 710 ;
|
||||
C 208 ; WX 600 ; N Eth ; B 43 0 645 562 ;
|
||||
C 199 ; WX 600 ; N Ccedilla ; B 93 -151 658 580 ;
|
||||
C -1 ; WX 600 ; N lcommaaccent ; B 95 -250 515 629 ;
|
||||
C -1 ; WX 600 ; N tcaron ; B 167 -15 587 717 ;
|
||||
C -1 ; WX 600 ; N eogonek ; B 106 -172 598 441 ;
|
||||
C -1 ; WX 600 ; N Uogonek ; B 124 -172 702 562 ;
|
||||
C 193 ; WX 600 ; N Aacute ; B 3 0 660 805 ;
|
||||
C 196 ; WX 600 ; N Adieresis ; B 3 0 607 753 ;
|
||||
C 232 ; WX 600 ; N egrave ; B 106 -15 598 672 ;
|
||||
C -1 ; WX 600 ; N zacute ; B 99 0 612 672 ;
|
||||
C -1 ; WX 600 ; N iogonek ; B 95 -172 515 657 ;
|
||||
C 211 ; WX 600 ; N Oacute ; B 94 -18 640 805 ;
|
||||
C 243 ; WX 600 ; N oacute ; B 102 -15 612 672 ;
|
||||
C -1 ; WX 600 ; N amacron ; B 76 -15 600 565 ;
|
||||
C -1 ; WX 600 ; N sacute ; B 78 -15 612 672 ;
|
||||
C 239 ; WX 600 ; N idieresis ; B 95 0 545 620 ;
|
||||
C 212 ; WX 600 ; N Ocircumflex ; B 94 -18 625 787 ;
|
||||
C 217 ; WX 600 ; N Ugrave ; B 125 -18 702 805 ;
|
||||
C -1 ; WX 600 ; N Delta ; B 6 0 598 688 ;
|
||||
C 254 ; WX 600 ; N thorn ; B -24 -157 605 629 ;
|
||||
C 178 ; WX 600 ; N twosuperior ; B 230 249 535 622 ;
|
||||
C 214 ; WX 600 ; N Odieresis ; B 94 -18 625 753 ;
|
||||
C 181 ; WX 600 ; N mu ; B 72 -157 572 426 ;
|
||||
C 236 ; WX 600 ; N igrave ; B 95 0 515 672 ;
|
||||
C -1 ; WX 600 ; N ohungarumlaut ; B 102 -15 723 672 ;
|
||||
C -1 ; WX 600 ; N Eogonek ; B 53 -172 660 562 ;
|
||||
C -1 ; WX 600 ; N dcroat ; B 85 -15 704 629 ;
|
||||
C 190 ; WX 600 ; N threequarters ; B 73 -56 659 666 ;
|
||||
C -1 ; WX 600 ; N Scedilla ; B 76 -151 650 580 ;
|
||||
C -1 ; WX 600 ; N lcaron ; B 95 0 667 629 ;
|
||||
C -1 ; WX 600 ; N Kcommaaccent ; B 38 -250 671 562 ;
|
||||
C -1 ; WX 600 ; N Lacute ; B 47 0 607 805 ;
|
||||
C 153 ; WX 600 ; N trademark ; B 75 263 742 562 ;
|
||||
C -1 ; WX 600 ; N edotaccent ; B 106 -15 598 620 ;
|
||||
C 204 ; WX 600 ; N Igrave ; B 96 0 623 805 ;
|
||||
C -1 ; WX 600 ; N Imacron ; B 96 0 628 698 ;
|
||||
C -1 ; WX 600 ; N Lcaron ; B 47 0 632 562 ;
|
||||
C 189 ; WX 600 ; N onehalf ; B 65 -57 669 665 ;
|
||||
C -1 ; WX 600 ; N lessequal ; B 98 0 645 710 ;
|
||||
C 244 ; WX 600 ; N ocircumflex ; B 102 -15 588 654 ;
|
||||
C 241 ; WX 600 ; N ntilde ; B 26 0 629 606 ;
|
||||
C -1 ; WX 600 ; N Uhungarumlaut ; B 125 -18 761 805 ;
|
||||
C 201 ; WX 600 ; N Eacute ; B 53 0 670 805 ;
|
||||
C -1 ; WX 600 ; N emacron ; B 106 -15 600 565 ;
|
||||
C -1 ; WX 600 ; N gbreve ; B 61 -157 657 609 ;
|
||||
C 188 ; WX 600 ; N onequarter ; B 65 -57 674 665 ;
|
||||
C 138 ; WX 600 ; N Scaron ; B 76 -20 672 802 ;
|
||||
C -1 ; WX 600 ; N Scommaaccent ; B 76 -250 650 580 ;
|
||||
C -1 ; WX 600 ; N Ohungarumlaut ; B 94 -18 751 805 ;
|
||||
C 176 ; WX 600 ; N degree ; B 214 269 576 622 ;
|
||||
C 242 ; WX 600 ; N ograve ; B 102 -15 588 672 ;
|
||||
C -1 ; WX 600 ; N Ccaron ; B 93 -18 672 802 ;
|
||||
C 249 ; WX 600 ; N ugrave ; B 101 -15 572 672 ;
|
||||
C -1 ; WX 600 ; N radical ; B 85 -15 765 792 ;
|
||||
C -1 ; WX 600 ; N Dcaron ; B 43 0 645 802 ;
|
||||
C -1 ; WX 600 ; N rcommaaccent ; B 60 -250 636 441 ;
|
||||
C 209 ; WX 600 ; N Ntilde ; B 7 -13 712 729 ;
|
||||
C 245 ; WX 600 ; N otilde ; B 102 -15 629 606 ;
|
||||
C -1 ; WX 600 ; N Rcommaaccent ; B 38 -250 598 562 ;
|
||||
C -1 ; WX 600 ; N Lcommaaccent ; B 47 -250 607 562 ;
|
||||
C 195 ; WX 600 ; N Atilde ; B 3 0 655 729 ;
|
||||
C -1 ; WX 600 ; N Aogonek ; B 3 -172 607 562 ;
|
||||
C 197 ; WX 600 ; N Aring ; B 3 0 607 750 ;
|
||||
C 213 ; WX 600 ; N Otilde ; B 94 -18 655 729 ;
|
||||
C -1 ; WX 600 ; N zdotaccent ; B 99 0 593 620 ;
|
||||
C -1 ; WX 600 ; N Ecaron ; B 53 0 660 802 ;
|
||||
C -1 ; WX 600 ; N Iogonek ; B 96 -172 623 562 ;
|
||||
C -1 ; WX 600 ; N kcommaaccent ; B 58 -250 633 629 ;
|
||||
C -1 ; WX 600 ; N minus ; B 129 232 580 283 ;
|
||||
C 206 ; WX 600 ; N Icircumflex ; B 96 0 623 787 ;
|
||||
C -1 ; WX 600 ; N ncaron ; B 26 0 614 669 ;
|
||||
C -1 ; WX 600 ; N tcommaaccent ; B 165 -250 561 561 ;
|
||||
C 172 ; WX 600 ; N logicalnot ; B 155 108 591 369 ;
|
||||
C 246 ; WX 600 ; N odieresis ; B 102 -15 588 620 ;
|
||||
C 252 ; WX 600 ; N udieresis ; B 101 -15 575 620 ;
|
||||
C -1 ; WX 600 ; N notequal ; B 43 -16 621 529 ;
|
||||
C -1 ; WX 600 ; N gcommaaccent ; B 61 -157 657 708 ;
|
||||
C 240 ; WX 600 ; N eth ; B 102 -15 639 629 ;
|
||||
C 158 ; WX 600 ; N zcaron ; B 99 0 624 669 ;
|
||||
C -1 ; WX 600 ; N ncommaaccent ; B 26 -250 585 441 ;
|
||||
C 185 ; WX 600 ; N onesuperior ; B 231 249 491 622 ;
|
||||
C -1 ; WX 600 ; N imacron ; B 95 0 543 565 ;
|
||||
C 128 ; WX 600 ; N Euro ; B 0 0 0 0 ;
|
||||
EndCharMetrics
|
||||
EndFontMetrics
|
||||
344
pirp/vendor/dompdf/dompdf/lib/fonts/Courier.afm
vendored
Normal file
344
pirp/vendor/dompdf/dompdf/lib/fonts/Courier.afm
vendored
Normal file
@@ -0,0 +1,344 @@
|
||||
StartFontMetrics 4.1
|
||||
Comment Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved.
|
||||
Comment Creation Date: Thu May 1 17:27:09 1997
|
||||
Comment UniqueID 43050
|
||||
Comment VMusage 39754 50779
|
||||
FontName Courier
|
||||
FullName Courier
|
||||
FamilyName Courier
|
||||
Weight Medium
|
||||
ItalicAngle 0
|
||||
IsFixedPitch true
|
||||
CharacterSet ExtendedRoman
|
||||
FontBBox -23 -250 715 805
|
||||
UnderlinePosition -100
|
||||
UnderlineThickness 50
|
||||
Version 003.000
|
||||
Notice Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved.
|
||||
EncodingScheme WinAnsiEncoding
|
||||
CapHeight 562
|
||||
XHeight 426
|
||||
Ascender 629
|
||||
Descender -157
|
||||
StdHW 51
|
||||
StdVW 51
|
||||
StartCharMetrics 317
|
||||
C 32 ; WX 600 ; N space ; B 0 0 0 0 ;
|
||||
C 160 ; WX 600 ; N space ; B 0 0 0 0 ;
|
||||
C 33 ; WX 600 ; N exclam ; B 236 -15 364 572 ;
|
||||
C 34 ; WX 600 ; N quotedbl ; B 187 328 413 562 ;
|
||||
C 35 ; WX 600 ; N numbersign ; B 93 -32 507 639 ;
|
||||
C 36 ; WX 600 ; N dollar ; B 105 -126 496 662 ;
|
||||
C 37 ; WX 600 ; N percent ; B 81 -15 518 622 ;
|
||||
C 38 ; WX 600 ; N ampersand ; B 63 -15 538 543 ;
|
||||
C 146 ; WX 600 ; N quoteright ; B 213 328 376 562 ;
|
||||
C 40 ; WX 600 ; N parenleft ; B 269 -108 440 622 ;
|
||||
C 41 ; WX 600 ; N parenright ; B 160 -108 331 622 ;
|
||||
C 42 ; WX 600 ; N asterisk ; B 116 257 484 607 ;
|
||||
C 43 ; WX 600 ; N plus ; B 80 44 520 470 ;
|
||||
C 44 ; WX 600 ; N comma ; B 181 -112 344 122 ;
|
||||
C 45 ; WX 600 ; N hyphen ; B 103 231 497 285 ;
|
||||
C 173 ; WX 600 ; N hyphen ; B 103 231 497 285 ;
|
||||
C 46 ; WX 600 ; N period ; B 229 -15 371 109 ;
|
||||
C 47 ; WX 600 ; N slash ; B 125 -80 475 629 ;
|
||||
C 48 ; WX 600 ; N zero ; B 106 -15 494 622 ;
|
||||
C 49 ; WX 600 ; N one ; B 96 0 505 622 ;
|
||||
C 50 ; WX 600 ; N two ; B 70 0 471 622 ;
|
||||
C 51 ; WX 600 ; N three ; B 75 -15 466 622 ;
|
||||
C 52 ; WX 600 ; N four ; B 78 0 500 622 ;
|
||||
C 53 ; WX 600 ; N five ; B 92 -15 497 607 ;
|
||||
C 54 ; WX 600 ; N six ; B 111 -15 497 622 ;
|
||||
C 55 ; WX 600 ; N seven ; B 82 0 483 607 ;
|
||||
C 56 ; WX 600 ; N eight ; B 102 -15 498 622 ;
|
||||
C 57 ; WX 600 ; N nine ; B 96 -15 489 622 ;
|
||||
C 58 ; WX 600 ; N colon ; B 229 -15 371 385 ;
|
||||
C 59 ; WX 600 ; N semicolon ; B 181 -112 371 385 ;
|
||||
C 60 ; WX 600 ; N less ; B 41 42 519 472 ;
|
||||
C 61 ; WX 600 ; N equal ; B 80 138 520 376 ;
|
||||
C 62 ; WX 600 ; N greater ; B 66 42 544 472 ;
|
||||
C 63 ; WX 600 ; N question ; B 129 -15 492 572 ;
|
||||
C 64 ; WX 600 ; N at ; B 77 -15 533 622 ;
|
||||
C 65 ; WX 600 ; N A ; B 3 0 597 562 ;
|
||||
C 66 ; WX 600 ; N B ; B 43 0 559 562 ;
|
||||
C 67 ; WX 600 ; N C ; B 41 -18 540 580 ;
|
||||
C 68 ; WX 600 ; N D ; B 43 0 574 562 ;
|
||||
C 69 ; WX 600 ; N E ; B 53 0 550 562 ;
|
||||
C 70 ; WX 600 ; N F ; B 53 0 545 562 ;
|
||||
C 71 ; WX 600 ; N G ; B 31 -18 575 580 ;
|
||||
C 72 ; WX 600 ; N H ; B 32 0 568 562 ;
|
||||
C 73 ; WX 600 ; N I ; B 96 0 504 562 ;
|
||||
C 74 ; WX 600 ; N J ; B 34 -18 566 562 ;
|
||||
C 75 ; WX 600 ; N K ; B 38 0 582 562 ;
|
||||
C 76 ; WX 600 ; N L ; B 47 0 554 562 ;
|
||||
C 77 ; WX 600 ; N M ; B 4 0 596 562 ;
|
||||
C 78 ; WX 600 ; N N ; B 7 -13 593 562 ;
|
||||
C 79 ; WX 600 ; N O ; B 43 -18 557 580 ;
|
||||
C 80 ; WX 600 ; N P ; B 79 0 558 562 ;
|
||||
C 81 ; WX 600 ; N Q ; B 43 -138 557 580 ;
|
||||
C 82 ; WX 600 ; N R ; B 38 0 588 562 ;
|
||||
C 83 ; WX 600 ; N S ; B 72 -20 529 580 ;
|
||||
C 84 ; WX 600 ; N T ; B 38 0 563 562 ;
|
||||
C 85 ; WX 600 ; N U ; B 17 -18 583 562 ;
|
||||
C 86 ; WX 600 ; N V ; B -4 -13 604 562 ;
|
||||
C 87 ; WX 600 ; N W ; B -3 -13 603 562 ;
|
||||
C 88 ; WX 600 ; N X ; B 23 0 577 562 ;
|
||||
C 89 ; WX 600 ; N Y ; B 24 0 576 562 ;
|
||||
C 90 ; WX 600 ; N Z ; B 86 0 514 562 ;
|
||||
C 91 ; WX 600 ; N bracketleft ; B 269 -108 442 622 ;
|
||||
C 92 ; WX 600 ; N backslash ; B 118 -80 482 629 ;
|
||||
C 93 ; WX 600 ; N bracketright ; B 158 -108 331 622 ;
|
||||
C 94 ; WX 600 ; N asciicircum ; B 94 354 506 622 ;
|
||||
C 95 ; WX 600 ; N underscore ; B 0 -125 600 -75 ;
|
||||
C 145 ; WX 600 ; N quoteleft ; B 224 328 387 562 ;
|
||||
C 97 ; WX 600 ; N a ; B 53 -15 559 441 ;
|
||||
C 98 ; WX 600 ; N b ; B 14 -15 575 629 ;
|
||||
C 99 ; WX 600 ; N c ; B 66 -15 529 441 ;
|
||||
C 100 ; WX 600 ; N d ; B 45 -15 591 629 ;
|
||||
C 101 ; WX 600 ; N e ; B 66 -15 548 441 ;
|
||||
C 102 ; WX 600 ; N f ; B 114 0 531 629 ; L i fi ; L l fl ;
|
||||
C 103 ; WX 600 ; N g ; B 45 -157 566 441 ;
|
||||
C 104 ; WX 600 ; N h ; B 18 0 582 629 ;
|
||||
C 105 ; WX 600 ; N i ; B 95 0 505 657 ;
|
||||
C 106 ; WX 600 ; N j ; B 82 -157 410 657 ;
|
||||
C 107 ; WX 600 ; N k ; B 43 0 580 629 ;
|
||||
C 108 ; WX 600 ; N l ; B 95 0 505 629 ;
|
||||
C 109 ; WX 600 ; N m ; B -5 0 605 441 ;
|
||||
C 110 ; WX 600 ; N n ; B 26 0 575 441 ;
|
||||
C 111 ; WX 600 ; N o ; B 62 -15 538 441 ;
|
||||
C 112 ; WX 600 ; N p ; B 9 -157 555 441 ;
|
||||
C 113 ; WX 600 ; N q ; B 45 -157 591 441 ;
|
||||
C 114 ; WX 600 ; N r ; B 60 0 559 441 ;
|
||||
C 115 ; WX 600 ; N s ; B 80 -15 513 441 ;
|
||||
C 116 ; WX 600 ; N t ; B 87 -15 530 561 ;
|
||||
C 117 ; WX 600 ; N u ; B 21 -15 562 426 ;
|
||||
C 118 ; WX 600 ; N v ; B 10 -10 590 426 ;
|
||||
C 119 ; WX 600 ; N w ; B -4 -10 604 426 ;
|
||||
C 120 ; WX 600 ; N x ; B 20 0 580 426 ;
|
||||
C 121 ; WX 600 ; N y ; B 7 -157 592 426 ;
|
||||
C 122 ; WX 600 ; N z ; B 99 0 502 426 ;
|
||||
C 123 ; WX 600 ; N braceleft ; B 182 -108 437 622 ;
|
||||
C 124 ; WX 600 ; N bar ; B 275 -250 326 750 ;
|
||||
C 125 ; WX 600 ; N braceright ; B 163 -108 418 622 ;
|
||||
C 126 ; WX 600 ; N asciitilde ; B 63 197 540 320 ;
|
||||
C 161 ; WX 600 ; N exclamdown ; B 236 -157 364 430 ;
|
||||
C 162 ; WX 600 ; N cent ; B 96 -49 500 614 ;
|
||||
C 163 ; WX 600 ; N sterling ; B 84 -21 521 611 ;
|
||||
C -1 ; WX 600 ; N fraction ; B 92 -57 509 665 ;
|
||||
C 165 ; WX 600 ; N yen ; B 26 0 574 562 ;
|
||||
C 131 ; WX 600 ; N florin ; B 4 -143 539 622 ;
|
||||
C 167 ; WX 600 ; N section ; B 113 -78 488 580 ;
|
||||
C 164 ; WX 600 ; N currency ; B 73 58 527 506 ;
|
||||
C 39 ; WX 600 ; N quotesingle ; B 259 328 341 562 ;
|
||||
C 147 ; WX 600 ; N quotedblleft ; B 143 328 471 562 ;
|
||||
C 171 ; WX 600 ; N guillemotleft ; B 37 70 563 446 ;
|
||||
C 139 ; WX 600 ; N guilsinglleft ; B 149 70 451 446 ;
|
||||
C 155 ; WX 600 ; N guilsinglright ; B 149 70 451 446 ;
|
||||
C -1 ; WX 600 ; N fi ; B 3 0 597 629 ;
|
||||
C -1 ; WX 600 ; N fl ; B 3 0 597 629 ;
|
||||
C 150 ; WX 600 ; N endash ; B 75 231 525 285 ;
|
||||
C 134 ; WX 600 ; N dagger ; B 141 -78 459 580 ;
|
||||
C 135 ; WX 600 ; N daggerdbl ; B 141 -78 459 580 ;
|
||||
C 183 ; WX 600 ; N periodcentered ; B 222 189 378 327 ;
|
||||
C 182 ; WX 600 ; N paragraph ; B 50 -78 511 562 ;
|
||||
C 149 ; WX 600 ; N bullet ; B 172 130 428 383 ;
|
||||
C 130 ; WX 600 ; N quotesinglbase ; B 213 -134 376 100 ;
|
||||
C 132 ; WX 600 ; N quotedblbase ; B 143 -134 457 100 ;
|
||||
C 148 ; WX 600 ; N quotedblright ; B 143 328 457 562 ;
|
||||
C 187 ; WX 600 ; N guillemotright ; B 37 70 563 446 ;
|
||||
C 133 ; WX 600 ; N ellipsis ; B 37 -15 563 111 ;
|
||||
C 137 ; WX 600 ; N perthousand ; B 3 -15 600 622 ;
|
||||
C 191 ; WX 600 ; N questiondown ; B 108 -157 471 430 ;
|
||||
C 96 ; WX 600 ; N grave ; B 151 497 378 672 ;
|
||||
C 180 ; WX 600 ; N acute ; B 242 497 469 672 ;
|
||||
C 136 ; WX 600 ; N circumflex ; B 124 477 476 654 ;
|
||||
C 152 ; WX 600 ; N tilde ; B 105 489 503 606 ;
|
||||
C 175 ; WX 600 ; N macron ; B 120 525 480 565 ;
|
||||
C -1 ; WX 600 ; N breve ; B 153 501 447 609 ;
|
||||
C -1 ; WX 600 ; N dotaccent ; B 249 537 352 640 ;
|
||||
C 168 ; WX 600 ; N dieresis ; B 148 537 453 640 ;
|
||||
C -1 ; WX 600 ; N ring ; B 218 463 382 627 ;
|
||||
C 184 ; WX 600 ; N cedilla ; B 224 -151 362 10 ;
|
||||
C -1 ; WX 600 ; N hungarumlaut ; B 133 497 540 672 ;
|
||||
C -1 ; WX 600 ; N ogonek ; B 211 -172 407 4 ;
|
||||
C -1 ; WX 600 ; N caron ; B 124 492 476 669 ;
|
||||
C 151 ; WX 600 ; N emdash ; B 0 231 600 285 ;
|
||||
C 198 ; WX 600 ; N AE ; B 3 0 550 562 ;
|
||||
C 170 ; WX 600 ; N ordfeminine ; B 156 249 442 580 ;
|
||||
C -1 ; WX 600 ; N Lslash ; B 47 0 554 562 ;
|
||||
C 216 ; WX 600 ; N Oslash ; B 43 -80 557 629 ;
|
||||
C 140 ; WX 600 ; N OE ; B 7 0 567 562 ;
|
||||
C 186 ; WX 600 ; N ordmasculine ; B 157 249 443 580 ;
|
||||
C 230 ; WX 600 ; N ae ; B 19 -15 570 441 ;
|
||||
C -1 ; WX 600 ; N dotlessi ; B 95 0 505 426 ;
|
||||
C -1 ; WX 600 ; N lslash ; B 95 0 505 629 ;
|
||||
C 248 ; WX 600 ; N oslash ; B 62 -80 538 506 ;
|
||||
C 156 ; WX 600 ; N oe ; B 19 -15 559 441 ;
|
||||
C 223 ; WX 600 ; N germandbls ; B 48 -15 588 629 ;
|
||||
C 207 ; WX 600 ; N Idieresis ; B 96 0 504 753 ;
|
||||
C 233 ; WX 600 ; N eacute ; B 66 -15 548 672 ;
|
||||
C -1 ; WX 600 ; N abreve ; B 53 -15 559 609 ;
|
||||
C -1 ; WX 600 ; N uhungarumlaut ; B 21 -15 580 672 ;
|
||||
C -1 ; WX 600 ; N ecaron ; B 66 -15 548 669 ;
|
||||
C 159 ; WX 600 ; N Ydieresis ; B 24 0 576 753 ;
|
||||
C 247 ; WX 600 ; N divide ; B 87 48 513 467 ;
|
||||
C 221 ; WX 600 ; N Yacute ; B 24 0 576 805 ;
|
||||
C 194 ; WX 600 ; N Acircumflex ; B 3 0 597 787 ;
|
||||
C 225 ; WX 600 ; N aacute ; B 53 -15 559 672 ;
|
||||
C 219 ; WX 600 ; N Ucircumflex ; B 17 -18 583 787 ;
|
||||
C 253 ; WX 600 ; N yacute ; B 7 -157 592 672 ;
|
||||
C -1 ; WX 600 ; N scommaaccent ; B 80 -250 513 441 ;
|
||||
C 234 ; WX 600 ; N ecircumflex ; B 66 -15 548 654 ;
|
||||
C -1 ; WX 600 ; N Uring ; B 17 -18 583 760 ;
|
||||
C 220 ; WX 600 ; N Udieresis ; B 17 -18 583 753 ;
|
||||
C -1 ; WX 600 ; N aogonek ; B 53 -172 587 441 ;
|
||||
C 218 ; WX 600 ; N Uacute ; B 17 -18 583 805 ;
|
||||
C -1 ; WX 600 ; N uogonek ; B 21 -172 590 426 ;
|
||||
C 203 ; WX 600 ; N Edieresis ; B 53 0 550 753 ;
|
||||
C -1 ; WX 600 ; N Dcroat ; B 30 0 574 562 ;
|
||||
C -1 ; WX 600 ; N commaaccent ; B 198 -250 335 -58 ;
|
||||
C 169 ; WX 600 ; N copyright ; B 0 -18 600 580 ;
|
||||
C -1 ; WX 600 ; N Emacron ; B 53 0 550 698 ;
|
||||
C -1 ; WX 600 ; N ccaron ; B 66 -15 529 669 ;
|
||||
C 229 ; WX 600 ; N aring ; B 53 -15 559 627 ;
|
||||
C -1 ; WX 600 ; N Ncommaaccent ; B 7 -250 593 562 ;
|
||||
C -1 ; WX 600 ; N lacute ; B 95 0 505 805 ;
|
||||
C 224 ; WX 600 ; N agrave ; B 53 -15 559 672 ;
|
||||
C -1 ; WX 600 ; N Tcommaaccent ; B 38 -250 563 562 ;
|
||||
C -1 ; WX 600 ; N Cacute ; B 41 -18 540 805 ;
|
||||
C 227 ; WX 600 ; N atilde ; B 53 -15 559 606 ;
|
||||
C -1 ; WX 600 ; N Edotaccent ; B 53 0 550 753 ;
|
||||
C 154 ; WX 600 ; N scaron ; B 80 -15 513 669 ;
|
||||
C -1 ; WX 600 ; N scedilla ; B 80 -151 513 441 ;
|
||||
C 237 ; WX 600 ; N iacute ; B 95 0 505 672 ;
|
||||
C -1 ; WX 600 ; N lozenge ; B 18 0 443 706 ;
|
||||
C -1 ; WX 600 ; N Rcaron ; B 38 0 588 802 ;
|
||||
C -1 ; WX 600 ; N Gcommaaccent ; B 31 -250 575 580 ;
|
||||
C 251 ; WX 600 ; N ucircumflex ; B 21 -15 562 654 ;
|
||||
C 226 ; WX 600 ; N acircumflex ; B 53 -15 559 654 ;
|
||||
C -1 ; WX 600 ; N Amacron ; B 3 0 597 698 ;
|
||||
C -1 ; WX 600 ; N rcaron ; B 60 0 559 669 ;
|
||||
C 231 ; WX 600 ; N ccedilla ; B 66 -151 529 441 ;
|
||||
C -1 ; WX 600 ; N Zdotaccent ; B 86 0 514 753 ;
|
||||
C 222 ; WX 600 ; N Thorn ; B 79 0 538 562 ;
|
||||
C -1 ; WX 600 ; N Omacron ; B 43 -18 557 698 ;
|
||||
C -1 ; WX 600 ; N Racute ; B 38 0 588 805 ;
|
||||
C -1 ; WX 600 ; N Sacute ; B 72 -20 529 805 ;
|
||||
C -1 ; WX 600 ; N dcaron ; B 45 -15 715 629 ;
|
||||
C -1 ; WX 600 ; N Umacron ; B 17 -18 583 698 ;
|
||||
C -1 ; WX 600 ; N uring ; B 21 -15 562 627 ;
|
||||
C 179 ; WX 600 ; N threesuperior ; B 155 240 406 622 ;
|
||||
C 210 ; WX 600 ; N Ograve ; B 43 -18 557 805 ;
|
||||
C 192 ; WX 600 ; N Agrave ; B 3 0 597 805 ;
|
||||
C -1 ; WX 600 ; N Abreve ; B 3 0 597 732 ;
|
||||
C 215 ; WX 600 ; N multiply ; B 87 43 515 470 ;
|
||||
C 250 ; WX 600 ; N uacute ; B 21 -15 562 672 ;
|
||||
C -1 ; WX 600 ; N Tcaron ; B 38 0 563 802 ;
|
||||
C -1 ; WX 600 ; N partialdiff ; B 17 -38 459 710 ;
|
||||
C 255 ; WX 600 ; N ydieresis ; B 7 -157 592 620 ;
|
||||
C -1 ; WX 600 ; N Nacute ; B 7 -13 593 805 ;
|
||||
C 238 ; WX 600 ; N icircumflex ; B 94 0 505 654 ;
|
||||
C 202 ; WX 600 ; N Ecircumflex ; B 53 0 550 787 ;
|
||||
C 228 ; WX 600 ; N adieresis ; B 53 -15 559 620 ;
|
||||
C 235 ; WX 600 ; N edieresis ; B 66 -15 548 620 ;
|
||||
C -1 ; WX 600 ; N cacute ; B 66 -15 529 672 ;
|
||||
C -1 ; WX 600 ; N nacute ; B 26 0 575 672 ;
|
||||
C -1 ; WX 600 ; N umacron ; B 21 -15 562 565 ;
|
||||
C -1 ; WX 600 ; N Ncaron ; B 7 -13 593 802 ;
|
||||
C 205 ; WX 600 ; N Iacute ; B 96 0 504 805 ;
|
||||
C 177 ; WX 600 ; N plusminus ; B 87 44 513 558 ;
|
||||
C 166 ; WX 600 ; N brokenbar ; B 275 -175 326 675 ;
|
||||
C 174 ; WX 600 ; N registered ; B 0 -18 600 580 ;
|
||||
C -1 ; WX 600 ; N Gbreve ; B 31 -18 575 732 ;
|
||||
C -1 ; WX 600 ; N Idotaccent ; B 96 0 504 753 ;
|
||||
C -1 ; WX 600 ; N summation ; B 15 -10 585 706 ;
|
||||
C 200 ; WX 600 ; N Egrave ; B 53 0 550 805 ;
|
||||
C -1 ; WX 600 ; N racute ; B 60 0 559 672 ;
|
||||
C -1 ; WX 600 ; N omacron ; B 62 -15 538 565 ;
|
||||
C -1 ; WX 600 ; N Zacute ; B 86 0 514 805 ;
|
||||
C 142 ; WX 600 ; N Zcaron ; B 86 0 514 802 ;
|
||||
C -1 ; WX 600 ; N greaterequal ; B 98 0 502 710 ;
|
||||
C 208 ; WX 600 ; N Eth ; B 30 0 574 562 ;
|
||||
C 199 ; WX 600 ; N Ccedilla ; B 41 -151 540 580 ;
|
||||
C -1 ; WX 600 ; N lcommaaccent ; B 95 -250 505 629 ;
|
||||
C -1 ; WX 600 ; N tcaron ; B 87 -15 530 717 ;
|
||||
C -1 ; WX 600 ; N eogonek ; B 66 -172 548 441 ;
|
||||
C -1 ; WX 600 ; N Uogonek ; B 17 -172 583 562 ;
|
||||
C 193 ; WX 600 ; N Aacute ; B 3 0 597 805 ;
|
||||
C 196 ; WX 600 ; N Adieresis ; B 3 0 597 753 ;
|
||||
C 232 ; WX 600 ; N egrave ; B 66 -15 548 672 ;
|
||||
C -1 ; WX 600 ; N zacute ; B 99 0 502 672 ;
|
||||
C -1 ; WX 600 ; N iogonek ; B 95 -172 505 657 ;
|
||||
C 211 ; WX 600 ; N Oacute ; B 43 -18 557 805 ;
|
||||
C 243 ; WX 600 ; N oacute ; B 62 -15 538 672 ;
|
||||
C -1 ; WX 600 ; N amacron ; B 53 -15 559 565 ;
|
||||
C -1 ; WX 600 ; N sacute ; B 80 -15 513 672 ;
|
||||
C 239 ; WX 600 ; N idieresis ; B 95 0 505 620 ;
|
||||
C 212 ; WX 600 ; N Ocircumflex ; B 43 -18 557 787 ;
|
||||
C 217 ; WX 600 ; N Ugrave ; B 17 -18 583 805 ;
|
||||
C -1 ; WX 600 ; N Delta ; B 6 0 598 688 ;
|
||||
C 254 ; WX 600 ; N thorn ; B -6 -157 555 629 ;
|
||||
C 178 ; WX 600 ; N twosuperior ; B 177 249 424 622 ;
|
||||
C 214 ; WX 600 ; N Odieresis ; B 43 -18 557 753 ;
|
||||
C 181 ; WX 600 ; N mu ; B 21 -157 562 426 ;
|
||||
C 236 ; WX 600 ; N igrave ; B 95 0 505 672 ;
|
||||
C -1 ; WX 600 ; N ohungarumlaut ; B 62 -15 580 672 ;
|
||||
C -1 ; WX 600 ; N Eogonek ; B 53 -172 561 562 ;
|
||||
C -1 ; WX 600 ; N dcroat ; B 45 -15 591 629 ;
|
||||
C 190 ; WX 600 ; N threequarters ; B 8 -56 593 666 ;
|
||||
C -1 ; WX 600 ; N Scedilla ; B 72 -151 529 580 ;
|
||||
C -1 ; WX 600 ; N lcaron ; B 95 0 533 629 ;
|
||||
C -1 ; WX 600 ; N Kcommaaccent ; B 38 -250 582 562 ;
|
||||
C -1 ; WX 600 ; N Lacute ; B 47 0 554 805 ;
|
||||
C 153 ; WX 600 ; N trademark ; B -23 263 623 562 ;
|
||||
C -1 ; WX 600 ; N edotaccent ; B 66 -15 548 620 ;
|
||||
C 204 ; WX 600 ; N Igrave ; B 96 0 504 805 ;
|
||||
C -1 ; WX 600 ; N Imacron ; B 96 0 504 698 ;
|
||||
C -1 ; WX 600 ; N Lcaron ; B 47 0 554 562 ;
|
||||
C 189 ; WX 600 ; N onehalf ; B 0 -57 611 665 ;
|
||||
C -1 ; WX 600 ; N lessequal ; B 98 0 502 710 ;
|
||||
C 244 ; WX 600 ; N ocircumflex ; B 62 -15 538 654 ;
|
||||
C 241 ; WX 600 ; N ntilde ; B 26 0 575 606 ;
|
||||
C -1 ; WX 600 ; N Uhungarumlaut ; B 17 -18 590 805 ;
|
||||
C 201 ; WX 600 ; N Eacute ; B 53 0 550 805 ;
|
||||
C -1 ; WX 600 ; N emacron ; B 66 -15 548 565 ;
|
||||
C -1 ; WX 600 ; N gbreve ; B 45 -157 566 609 ;
|
||||
C 188 ; WX 600 ; N onequarter ; B 0 -57 600 665 ;
|
||||
C 138 ; WX 600 ; N Scaron ; B 72 -20 529 802 ;
|
||||
C -1 ; WX 600 ; N Scommaaccent ; B 72 -250 529 580 ;
|
||||
C -1 ; WX 600 ; N Ohungarumlaut ; B 43 -18 580 805 ;
|
||||
C 176 ; WX 600 ; N degree ; B 123 269 477 622 ;
|
||||
C 242 ; WX 600 ; N ograve ; B 62 -15 538 672 ;
|
||||
C -1 ; WX 600 ; N Ccaron ; B 41 -18 540 802 ;
|
||||
C 249 ; WX 600 ; N ugrave ; B 21 -15 562 672 ;
|
||||
C -1 ; WX 600 ; N radical ; B 3 -15 597 792 ;
|
||||
C -1 ; WX 600 ; N Dcaron ; B 43 0 574 802 ;
|
||||
C -1 ; WX 600 ; N rcommaaccent ; B 60 -250 559 441 ;
|
||||
C 209 ; WX 600 ; N Ntilde ; B 7 -13 593 729 ;
|
||||
C 245 ; WX 600 ; N otilde ; B 62 -15 538 606 ;
|
||||
C -1 ; WX 600 ; N Rcommaaccent ; B 38 -250 588 562 ;
|
||||
C -1 ; WX 600 ; N Lcommaaccent ; B 47 -250 554 562 ;
|
||||
C 195 ; WX 600 ; N Atilde ; B 3 0 597 729 ;
|
||||
C -1 ; WX 600 ; N Aogonek ; B 3 -172 608 562 ;
|
||||
C 197 ; WX 600 ; N Aring ; B 3 0 597 750 ;
|
||||
C 213 ; WX 600 ; N Otilde ; B 43 -18 557 729 ;
|
||||
C -1 ; WX 600 ; N zdotaccent ; B 99 0 502 620 ;
|
||||
C -1 ; WX 600 ; N Ecaron ; B 53 0 550 802 ;
|
||||
C -1 ; WX 600 ; N Iogonek ; B 96 -172 504 562 ;
|
||||
C -1 ; WX 600 ; N kcommaaccent ; B 43 -250 580 629 ;
|
||||
C -1 ; WX 600 ; N minus ; B 80 232 520 283 ;
|
||||
C 206 ; WX 600 ; N Icircumflex ; B 96 0 504 787 ;
|
||||
C -1 ; WX 600 ; N ncaron ; B 26 0 575 669 ;
|
||||
C -1 ; WX 600 ; N tcommaaccent ; B 87 -250 530 561 ;
|
||||
C 172 ; WX 600 ; N logicalnot ; B 87 108 513 369 ;
|
||||
C 246 ; WX 600 ; N odieresis ; B 62 -15 538 620 ;
|
||||
C 252 ; WX 600 ; N udieresis ; B 21 -15 562 620 ;
|
||||
C -1 ; WX 600 ; N notequal ; B 15 -16 540 529 ;
|
||||
C -1 ; WX 600 ; N gcommaaccent ; B 45 -157 566 708 ;
|
||||
C 240 ; WX 600 ; N eth ; B 62 -15 538 629 ;
|
||||
C 158 ; WX 600 ; N zcaron ; B 99 0 502 669 ;
|
||||
C -1 ; WX 600 ; N ncommaaccent ; B 26 -250 575 441 ;
|
||||
C 185 ; WX 600 ; N onesuperior ; B 172 249 428 622 ;
|
||||
C -1 ; WX 600 ; N imacron ; B 95 0 505 565 ;
|
||||
C 128 ; WX 600 ; N Euro ; B 0 0 0 0 ;
|
||||
EndCharMetrics
|
||||
EndFontMetrics
|
||||
BIN
pirp/vendor/dompdf/dompdf/lib/fonts/DejaVuSans-Bold.ttf
vendored
Normal file
BIN
pirp/vendor/dompdf/dompdf/lib/fonts/DejaVuSans-Bold.ttf
vendored
Normal file
Binary file not shown.
6067
pirp/vendor/dompdf/dompdf/lib/fonts/DejaVuSans-Bold.ufm
vendored
Normal file
6067
pirp/vendor/dompdf/dompdf/lib/fonts/DejaVuSans-Bold.ufm
vendored
Normal file
File diff suppressed because it is too large
Load Diff
10762
pirp/vendor/dompdf/dompdf/lib/fonts/DejaVuSans-Bold.ufm.json
vendored
Normal file
10762
pirp/vendor/dompdf/dompdf/lib/fonts/DejaVuSans-Bold.ufm.json
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
pirp/vendor/dompdf/dompdf/lib/fonts/DejaVuSans-BoldOblique.ttf
vendored
Normal file
BIN
pirp/vendor/dompdf/dompdf/lib/fonts/DejaVuSans-BoldOblique.ttf
vendored
Normal file
Binary file not shown.
5712
pirp/vendor/dompdf/dompdf/lib/fonts/DejaVuSans-BoldOblique.ufm
vendored
Normal file
5712
pirp/vendor/dompdf/dompdf/lib/fonts/DejaVuSans-BoldOblique.ufm
vendored
Normal file
File diff suppressed because it is too large
Load Diff
BIN
pirp/vendor/dompdf/dompdf/lib/fonts/DejaVuSans-Oblique.ttf
vendored
Normal file
BIN
pirp/vendor/dompdf/dompdf/lib/fonts/DejaVuSans-Oblique.ttf
vendored
Normal file
Binary file not shown.
5268
pirp/vendor/dompdf/dompdf/lib/fonts/DejaVuSans-Oblique.ufm
vendored
Normal file
5268
pirp/vendor/dompdf/dompdf/lib/fonts/DejaVuSans-Oblique.ufm
vendored
Normal file
File diff suppressed because it is too large
Load Diff
BIN
pirp/vendor/dompdf/dompdf/lib/fonts/DejaVuSans.ttf
vendored
Normal file
BIN
pirp/vendor/dompdf/dompdf/lib/fonts/DejaVuSans.ttf
vendored
Normal file
Binary file not shown.
6661
pirp/vendor/dompdf/dompdf/lib/fonts/DejaVuSans.ufm
vendored
Normal file
6661
pirp/vendor/dompdf/dompdf/lib/fonts/DejaVuSans.ufm
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user