feat: add signer name and title fields to invoices with toggleable UI visibility

This commit is contained in:
I-Fun
2026-04-05 13:10:07 +07:00
parent 12d44cbecc
commit ed9e7dc0fd
8 changed files with 178 additions and 93 deletions

View File

@@ -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',

View File

@@ -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 = [

View File

@@ -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']);
});
}
};

View File

@@ -20,11 +20,27 @@ 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
echo "[db] Creating SQLite database file…" echo "[db] Creating SQLite database file…"

View File

@@ -123,31 +123,62 @@
</div> </div>
</div> </div>
{{-- Bank Info --}} <div class="mb-5 flex items-center gap-2 bg-pink-50/50 p-4 rounded-2xl border border-pink-100">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-5"> <input type="hidden" name="use_custom_fields" value="0">
<div> <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 class="block text-xs font-semibold text-gray-600 mb-1">Bank Name</label> <label for="useCustomFields" class="text-sm font-bold text-gray-700 cursor-pointer select-none">Show Custom Bank & Signature Settings</label>
<input type="text" name="bank_name" </div>
value="{{ old('bank_name', $invoiceSettings->default_bank_name) }}" class="input-soft text-sm"
placeholder="BCA, Mandiri..."> <div x-show="showCustomFields" x-collapse>
{{-- Bank Info --}}
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-5">
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Bank Name</label>
<input type="text" name="bank_name"
value="{{ old('bank_name', $invoiceSettings->default_bank_name) }}" class="input-soft text-sm"
placeholder="BCA, Mandiri...">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Branch Name</label>
<input type="text" name="branch_name"
value="{{ old('branch_name', $invoiceSettings->default_branch_name) }}"
class="input-soft text-sm" placeholder="e.g. KCP Sudirman">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Account Number</label>
<input type="text" name="bank_account"
value="{{ old('bank_account', $invoiceSettings->default_bank_account) }}"
class="input-soft text-sm">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Account Holder</label>
<input type="text" name="bank_holder"
value="{{ old('bank_holder', $invoiceSettings->default_bank_holder) }}"
class="input-soft text-sm">
</div>
</div> </div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Branch Name</label> {{-- Custom Signature Text --}}
<input type="text" name="branch_name" <div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-5">
value="{{ old('branch_name', $invoiceSettings->default_branch_name) }}" <div>
class="input-soft text-sm" placeholder="e.g. KCP Sudirman"> <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>
<label class="block text-xs font-semibold text-gray-600 mb-1">Account Number</label> <div class="mb-5">
<input type="text" name="bank_account" <label class="block text-sm font-semibold text-gray-700 mb-1">Custom Signature Image (Optional)</label>
value="{{ old('bank_account', $invoiceSettings->default_bank_account) }}" <p class="text-xs text-gray-400 mb-2">Leave blank to use your default signature from settings.</p>
class="input-soft text-sm"> <input type="file" name="signature_image" accept="image/*" class="w-full text-sm text-gray-600
</div> file:mr-3 file:py-1.5 file:px-4
<div> file:rounded-full file:border-0
<label class="block text-xs font-semibold text-gray-600 mb-1">Account Holder</label> file:text-xs file:font-semibold
<input type="text" name="bank_holder" file:bg-pink-50 file:text-pink-600
value="{{ old('bank_holder', $invoiceSettings->default_bank_holder) }}" hover:file:bg-pink-100 cursor-pointer">
class="input-soft text-sm">
</div> </div>
</div> </div>
@@ -157,17 +188,6 @@
placeholder="Payment terms, etc.">{{ old('notes', $invoiceSettings->default_notes) }}</textarea> placeholder="Payment terms, etc.">{{ old('notes', $invoiceSettings->default_notes) }}</textarea>
</div> </div>
<div class="mb-5">
<label class="block text-sm font-semibold text-gray-700 mb-1">Custom Signature Image (Optional)</label>
<p class="text-xs text-gray-400 mb-2">Leave blank to use your default signature from settings.</p>
<input type="file" name="signature_image" accept="image/*" class="w-full text-sm text-gray-600
file:mr-3 file:py-1.5 file:px-4
file:rounded-full file:border-0
file:text-xs file:font-semibold
file:bg-pink-50 file:text-pink-600
hover:file:bg-pink-100 cursor-pointer">
</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>
<a href="{{ route('invoices.index') }}" <a href="{{ route('invoices.index') }}"
@@ -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() {

View File

@@ -121,27 +121,77 @@
</div> </div>
</div> </div>
{{-- Bank Info --}} <div class="mb-5 flex items-center gap-2 bg-pink-50/50 p-4 rounded-2xl border border-pink-100">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-5"> <input type="hidden" name="use_custom_fields" value="0">
<div> <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 class="block text-xs font-semibold text-gray-600 mb-1">Bank Name</label> <label for="useCustomFields" class="text-sm font-bold text-gray-700 cursor-pointer select-none">Show Custom Bank & Signature Settings</label>
<input type="text" name="bank_name" value="{{ old('bank_name', $invoice->bank_name) }}" </div>
class="input-soft text-sm">
<div x-show="showCustomFields" x-collapse>
{{-- Bank Info --}}
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-5">
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Bank Name</label>
<input type="text" name="bank_name" value="{{ old('bank_name', $invoice->bank_name) }}"
class="input-soft text-sm">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Branch Name</label>
<input type="text" name="branch_name" value="{{ old('branch_name', $invoice->branch_name) }}"
class="input-soft text-sm" placeholder="e.g. KCP Sudirman">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Account Number</label>
<input type="text" name="bank_account" value="{{ old('bank_account', $invoice->bank_account) }}"
class="input-soft text-sm">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Account Holder</label>
<input type="text" name="bank_holder" value="{{ old('bank_holder', $invoice->bank_holder) }}"
class="input-soft text-sm">
</div>
</div> </div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Branch Name</label> {{-- Custom Signature Text --}}
<input type="text" name="branch_name" value="{{ old('branch_name', $invoice->branch_name) }}" <div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-5">
class="input-soft text-sm" placeholder="e.g. KCP Sudirman"> <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>
<label class="block text-xs font-semibold text-gray-600 mb-1">Account Number</label> <div class="mb-5" x-data="{
<input type="text" name="bank_account" value="{{ old('bank_account', $invoice->bank_account) }}" removeSign() {
class="input-soft text-sm"> if (!confirm('Remove the signature from this invoice?')) return;
</div> $refs.removeSignInput.value = '1';
<div> this.$el.closest('form').submit();
<label class="block text-xs font-semibold text-gray-600 mb-1">Account Holder</label> }
<input type="text" name="bank_holder" value="{{ old('bank_holder', $invoice->bank_holder) }}" }">
class="input-soft text-sm"> <label class="block text-sm font-semibold text-gray-700 mb-1">Custom Signature Image (Optional)</label>
<p class="text-xs text-gray-400 mb-3">Upload a specific signature for this invoice. Leave blank to keep the current one.</p>
@if($invoice->signature_image)
<div class="flex items-center gap-4 mb-3 p-3 bg-white rounded-xl border border-pink-100">
<img src="{{ '/storage/' . $invoice->signature_image }}" alt="Signature" class="h-16 object-contain">
<div>
<input type="hidden" name="remove_signature" value="0" x-ref="removeSignInput">
<button type="button" @click="removeSign()" class="text-xs px-3 py-1.5 rounded-full border border-red-200 text-red-400 hover:bg-red-50">
🗑️ Remove
</button>
</div>
</div>
@endif
<input type="file" name="signature_image" accept="image/*" class="w-full text-sm text-gray-600
file:mr-3 file:py-1.5 file:px-4
file:rounded-full file:border-0
file:text-xs file:font-semibold
file:bg-pink-50 file:text-pink-600
hover:file:bg-pink-100 cursor-pointer">
</div> </div>
</div> </div>
@@ -149,36 +199,6 @@
<label class="block text-sm font-semibold text-gray-700 mb-1">Notes / Payment Terms</label> <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> <textarea name="notes" rows="2" class="input-soft resize-none">{{ old('notes', $invoice->notes) }}</textarea>
</div> </div>
<div class="mb-5" x-data="{
removeSign() {
if (!confirm('Remove the signature from this invoice?')) return;
$refs.removeSignInput.value = '1';
this.$el.closest('form').submit();
}
}">
<label class="block text-sm font-semibold text-gray-700 mb-1">Custom Signature Image (Optional)</label>
<p class="text-xs text-gray-400 mb-3">Upload a specific signature for this invoice. Leave blank to keep the current one.</p>
@if($invoice->signature_image)
<div class="flex items-center gap-4 mb-3 p-3 bg-white rounded-xl border border-pink-100">
<img src="{{ '/storage/' . $invoice->signature_image }}" alt="Signature" class="h-16 object-contain">
<div>
<input type="hidden" name="remove_signature" value="0" x-ref="removeSignInput">
<button type="button" @click="removeSign()" class="text-xs px-3 py-1.5 rounded-full border border-red-200 text-red-400 hover:bg-red-50">
🗑️ Remove
</button>
</div>
</div>
@endif
<input type="file" name="signature_image" accept="image/*" class="w-full text-sm text-gray-600
file:mr-3 file:py-1.5 file:px-4
file:rounded-full file:border-0
file:text-xs file:font-semibold
file:bg-pink-50 file:text-pink-600
hover:file:bg-pink-100 cursor-pointer">
</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() {

View File

@@ -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>

View File

@@ -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>