feat: add signer name and title fields to invoices with toggleable UI visibility
This commit is contained in:
@@ -46,6 +46,8 @@ class InvoiceController extends Controller
|
|||||||
'due_date' => 'required|date|after_or_equal:issue_date',
|
'due_date' => 'required|date|after_or_equal:issue_date',
|
||||||
'notes' => 'nullable|string',
|
'notes' => 'nullable|string',
|
||||||
'signature_image' => 'nullable|image|max:2048',
|
'signature_image' => 'nullable|image|max:2048',
|
||||||
|
'signer_name' => 'nullable|string|max:255',
|
||||||
|
'signer_title' => 'nullable|string|max:255',
|
||||||
'bank_name' => 'nullable|string|max:100',
|
'bank_name' => 'nullable|string|max:100',
|
||||||
'bank_account' => 'nullable|string|max:50',
|
'bank_account' => 'nullable|string|max:50',
|
||||||
'bank_holder' => 'nullable|string|max:100',
|
'bank_holder' => 'nullable|string|max:100',
|
||||||
@@ -108,6 +110,8 @@ class InvoiceController extends Controller
|
|||||||
'paid_date' => 'nullable|date',
|
'paid_date' => 'nullable|date',
|
||||||
'notes' => 'nullable|string',
|
'notes' => 'nullable|string',
|
||||||
'signature_image' => 'nullable|image|max:2048',
|
'signature_image' => 'nullable|image|max:2048',
|
||||||
|
'signer_name' => 'nullable|string|max:255',
|
||||||
|
'signer_title' => 'nullable|string|max:255',
|
||||||
'bank_name' => 'nullable|string|max:100',
|
'bank_name' => 'nullable|string|max:100',
|
||||||
'bank_account' => 'nullable|string|max:50',
|
'bank_account' => 'nullable|string|max:50',
|
||||||
'bank_holder' => 'nullable|string|max:100',
|
'bank_holder' => 'nullable|string|max:100',
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class Invoice extends Model
|
|||||||
'subtotal', 'tax_rate', 'tax_amount', 'total', 'currency',
|
'subtotal', 'tax_rate', 'tax_amount', 'total', 'currency',
|
||||||
'status', 'issue_date', 'due_date', 'paid_date', 'notes',
|
'status', 'issue_date', 'due_date', 'paid_date', 'notes',
|
||||||
'bank_name', 'bank_account', 'bank_holder', 'branch_name',
|
'bank_name', 'bank_account', 'bank_holder', 'branch_name',
|
||||||
'signature_image',
|
'signature_image', 'signer_name', 'signer_title',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('invoices', function (Blueprint $table) {
|
||||||
|
$table->string('signer_name')->nullable()->after('signature_image');
|
||||||
|
$table->string('signer_title')->nullable()->after('signer_name');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('invoices', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['signer_name', 'signer_title']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -20,10 +20,26 @@ if [ ! -f .env ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ── 2. Generate APP_KEY if empty ────────────
|
# ── 2. Generate APP_KEY if empty ────────────
|
||||||
if grep -qE '^APP_KEY=$' .env 2>/dev/null; then
|
if ! grep -qE '^APP_KEY=base64:' .env 2>/dev/null; then
|
||||||
echo "[key] Generating application key…"
|
echo "[key] Generating application key directly…"
|
||||||
php artisan key:generate --force --quiet
|
|
||||||
|
# 2a. Clear cached configs that might cause MissingAppKeyException
|
||||||
|
php artisan config:clear --quiet 2>/dev/null || true
|
||||||
|
|
||||||
|
# 2b. Generate native base64 key
|
||||||
|
NEW_KEY="base64:$(php -r 'echo base64_encode(random_bytes(32));')"
|
||||||
|
|
||||||
|
if grep -q '^APP_KEY=' .env 2>/dev/null; then
|
||||||
|
# Replace existing empty/invalid APP_KEY safely
|
||||||
|
sed -i "s|^APP_KEY=.*|APP_KEY=$NEW_KEY|" .env
|
||||||
|
else
|
||||||
|
# Append nicely
|
||||||
|
echo "APP_KEY=$NEW_KEY" >> .env
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2c. Force Laravel to read from .env by ditching Docker's empty OS injection
|
||||||
|
unset APP_KEY
|
||||||
|
|
||||||
# ── 3. Ensure SQLite database file exists ───
|
# ── 3. Ensure SQLite database file exists ───
|
||||||
if [ ! -f database/database.sqlite ]; then
|
if [ ! -f database/database.sqlite ]; then
|
||||||
|
|||||||
@@ -123,6 +123,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-5 flex items-center gap-2 bg-pink-50/50 p-4 rounded-2xl border border-pink-100">
|
||||||
|
<input type="hidden" name="use_custom_fields" value="0">
|
||||||
|
<input type="checkbox" id="useCustomFields" name="use_custom_fields" value="1" x-model="showCustomFields" class="w-4 h-4 text-pink-600 bg-white border-pink-300 rounded focus:ring-pink-500 cursor-pointer">
|
||||||
|
<label for="useCustomFields" class="text-sm font-bold text-gray-700 cursor-pointer select-none">Show Custom Bank & Signature Settings</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="showCustomFields" x-collapse>
|
||||||
{{-- Bank Info --}}
|
{{-- Bank Info --}}
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-5">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-5">
|
||||||
<div>
|
<div>
|
||||||
@@ -151,10 +158,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-5">
|
{{-- Custom Signature Text --}}
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-1">Notes / Payment Terms</label>
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-5">
|
||||||
<textarea name="notes" rows="2" class="input-soft resize-none"
|
<div>
|
||||||
placeholder="Payment terms, etc.">{{ old('notes', $invoiceSettings->default_notes) }}</textarea>
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Signer Name</label>
|
||||||
|
<input type="text" name="signer_name" value="{{ old('signer_name') }}" class="input-soft text-sm" placeholder="e.g. Cathy">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Signer Title</label>
|
||||||
|
<input type="text" name="signer_title" value="{{ old('signer_title') }}" class="input-soft text-sm" placeholder="e.g. Director">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
@@ -167,6 +180,13 @@
|
|||||||
file:bg-pink-50 file:text-pink-600
|
file:bg-pink-50 file:text-pink-600
|
||||||
hover:file:bg-pink-100 cursor-pointer">
|
hover:file:bg-pink-100 cursor-pointer">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-1">Notes / Payment Terms</label>
|
||||||
|
<textarea name="notes" rows="2" class="input-soft resize-none"
|
||||||
|
placeholder="Payment terms, etc.">{{ old('notes', $invoiceSettings->default_notes) }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col sm:flex-row gap-3">
|
<div class="flex flex-col sm:flex-row gap-3">
|
||||||
<button type="submit" class="btn-sakura flex-1 py-3">🧾 Create Invoice</button>
|
<button type="submit" class="btn-sakura flex-1 py-3">🧾 Create Invoice</button>
|
||||||
@@ -183,6 +203,7 @@
|
|||||||
<script>
|
<script>
|
||||||
function invoiceForm() {
|
function invoiceForm() {
|
||||||
return {
|
return {
|
||||||
|
showCustomFields: {{ old('use_custom_fields', false) ? 'true' : 'false' }},
|
||||||
items: [{ description: @json($invoiceSettings->default_service_description ?? ''), quantity: 1, unit_price: 0, is_taxable: true }],
|
items: [{ description: @json($invoiceSettings->default_service_description ?? ''), quantity: 1, unit_price: 0, is_taxable: true }],
|
||||||
taxRate: {{ $invoiceSettings->default_tax_rate ?? 11 }},
|
taxRate: {{ $invoiceSettings->default_tax_rate ?? 11 }},
|
||||||
get subtotal() {
|
get subtotal() {
|
||||||
|
|||||||
@@ -121,6 +121,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-5 flex items-center gap-2 bg-pink-50/50 p-4 rounded-2xl border border-pink-100">
|
||||||
|
<input type="hidden" name="use_custom_fields" value="0">
|
||||||
|
<input type="checkbox" id="useCustomFields" name="use_custom_fields" value="1" x-model="showCustomFields" class="w-4 h-4 text-pink-600 bg-white border-pink-300 rounded focus:ring-pink-500 cursor-pointer">
|
||||||
|
<label for="useCustomFields" class="text-sm font-bold text-gray-700 cursor-pointer select-none">Show Custom Bank & Signature Settings</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="showCustomFields" x-collapse>
|
||||||
{{-- Bank Info --}}
|
{{-- Bank Info --}}
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-5">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-5">
|
||||||
<div>
|
<div>
|
||||||
@@ -145,9 +152,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-5">
|
{{-- Custom Signature Text --}}
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-1">Notes / Payment Terms</label>
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-5">
|
||||||
<textarea name="notes" rows="2" class="input-soft resize-none">{{ old('notes', $invoice->notes) }}</textarea>
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Signer Name</label>
|
||||||
|
<input type="text" name="signer_name" value="{{ old('signer_name', $invoice->signer_name) }}" class="input-soft text-sm" placeholder="e.g. Cathy">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Signer Title</label>
|
||||||
|
<input type="text" name="signer_title" value="{{ old('signer_title', $invoice->signer_title) }}" class="input-soft text-sm" placeholder="e.g. Director">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-5" x-data="{
|
<div class="mb-5" x-data="{
|
||||||
@@ -179,6 +193,12 @@
|
|||||||
file:bg-pink-50 file:text-pink-600
|
file:bg-pink-50 file:text-pink-600
|
||||||
hover:file:bg-pink-100 cursor-pointer">
|
hover:file:bg-pink-100 cursor-pointer">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-1">Notes / Payment Terms</label>
|
||||||
|
<textarea name="notes" rows="2" class="input-soft resize-none">{{ old('notes', $invoice->notes) }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col sm:flex-row gap-3">
|
<div class="flex flex-col sm:flex-row gap-3">
|
||||||
<button type="submit" class="btn-sakura flex-1 py-3">💾 Save Changes</button>
|
<button type="submit" class="btn-sakura flex-1 py-3">💾 Save Changes</button>
|
||||||
@@ -200,6 +220,7 @@ function invoiceEditForm() {
|
|||||||
});
|
});
|
||||||
@endphp
|
@endphp
|
||||||
return {
|
return {
|
||||||
|
showCustomFields: {{ old('use_custom_fields', ($invoice->bank_name || $invoice->signature_image || $invoice->signer_name || $invoice->signer_title) ? true : false) ? 'true' : 'false' }},
|
||||||
items: @json($itemsJson),
|
items: @json($itemsJson),
|
||||||
taxRate: {{ $invoice->tax_rate }},
|
taxRate: {{ $invoice->tax_rate }},
|
||||||
get subtotal() {
|
get subtotal() {
|
||||||
|
|||||||
@@ -373,11 +373,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<img src="{{ storage_path('app/public/' . $invoice->signature_image) }}" style="max-height: 80px; max-width: 180px; object-fit: contain; margin-bottom: 8px;">
|
<img src="{{ storage_path('app/public/' . $invoice->signature_image) }}" style="max-height: 80px; max-width: 180px; object-fit: contain; margin-bottom: 8px;">
|
||||||
<div style="font-size: 12px; font-weight: 800; color: #111827; text-transform: uppercase;">
|
<div style="font-size: 12px; font-weight: 800; color: #111827; text-transform: uppercase;">
|
||||||
{{ $invoiceSettings->signer_name ?: ($invoiceSettings->header_name ?: 'Signature') }}
|
{{ $invoice->signer_name ?: ($invoiceSettings->signer_name ?: ($invoiceSettings->header_name ?: 'Signature')) }}
|
||||||
</div>
|
</div>
|
||||||
@if($invoiceSettings->signer_title || $invoiceSettings->header_title)
|
@if($invoice->signer_title || $invoiceSettings->signer_title || $invoiceSettings->header_title)
|
||||||
<div style="font-size: 10px; color: #6B7280; text-transform: uppercase; margin-top: 3px;">
|
<div style="font-size: 10px; color: #6B7280; text-transform: uppercase; margin-top: 3px;">
|
||||||
{{ $invoiceSettings->signer_title ?: $invoiceSettings->header_title }}
|
{{ $invoice->signer_title ?: ($invoiceSettings->signer_title ?: $invoiceSettings->header_title) }}
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -160,11 +160,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<img src="{{ '/storage/' . $invoice->signature_image }}" alt="Signature" class="h-20 object-contain mb-3 mx-auto">
|
<img src="{{ '/storage/' . $invoice->signature_image }}" alt="Signature" class="h-20 object-contain mb-3 mx-auto">
|
||||||
<div class="text-sm font-extrabold text-gray-800 uppercase tracking-wide">
|
<div class="text-sm font-extrabold text-gray-800 uppercase tracking-wide">
|
||||||
{{ $invoiceSettings->signer_name ?: ($invoiceSettings->header_name ?: 'Signature') }}
|
{{ $invoice->signer_name ?: ($invoiceSettings->signer_name ?: ($invoiceSettings->header_name ?: 'Signature')) }}
|
||||||
</div>
|
</div>
|
||||||
@if($invoiceSettings->signer_title || $invoiceSettings->header_title)
|
@if($invoice->signer_title || $invoiceSettings->signer_title || $invoiceSettings->header_title)
|
||||||
<div class="text-xs text-gray-400 uppercase mt-0.5 tracking-wider">
|
<div class="text-xs text-gray-400 uppercase mt-0.5 tracking-wider">
|
||||||
{{ $invoiceSettings->signer_title ?: $invoiceSettings->header_title }}
|
{{ $invoice->signer_title ?: ($invoiceSettings->signer_title ?: $invoiceSettings->header_title) }}
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user