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',
|
||||
'notes' => 'nullable|string',
|
||||
'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_account' => 'nullable|string|max:50',
|
||||
'bank_holder' => 'nullable|string|max:100',
|
||||
@@ -108,6 +110,8 @@ class InvoiceController extends Controller
|
||||
'paid_date' => 'nullable|date',
|
||||
'notes' => 'nullable|string',
|
||||
'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_account' => 'nullable|string|max:50',
|
||||
'bank_holder' => 'nullable|string|max:100',
|
||||
|
||||
@@ -21,7 +21,7 @@ class Invoice extends Model
|
||||
'subtotal', 'tax_rate', 'tax_amount', 'total', 'currency',
|
||||
'status', 'issue_date', 'due_date', 'paid_date', 'notes',
|
||||
'bank_name', 'bank_account', 'bank_holder', 'branch_name',
|
||||
'signature_image',
|
||||
'signature_image', 'signer_name', 'signer_title',
|
||||
];
|
||||
|
||||
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,11 +20,27 @@ if [ ! -f .env ]; then
|
||||
fi
|
||||
|
||||
# ── 2. Generate APP_KEY if empty ────────────
|
||||
if grep -qE '^APP_KEY=$' .env 2>/dev/null; then
|
||||
echo "[key] Generating application key…"
|
||||
php artisan key:generate --force --quiet
|
||||
if ! grep -qE '^APP_KEY=base64:' .env 2>/dev/null; then
|
||||
echo "[key] Generating application key directly…"
|
||||
|
||||
# 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
|
||||
|
||||
# 2c. Force Laravel to read from .env by ditching Docker's empty OS injection
|
||||
unset APP_KEY
|
||||
|
||||
# ── 3. Ensure SQLite database file exists ───
|
||||
if [ ! -f database/database.sqlite ]; then
|
||||
echo "[db] Creating SQLite database file…"
|
||||
|
||||
@@ -123,31 +123,62 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 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 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 --}}
|
||||
<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>
|
||||
<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">
|
||||
|
||||
{{-- Custom Signature Text --}}
|
||||
<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">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>
|
||||
<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 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>
|
||||
|
||||
@@ -157,17 +188,6 @@
|
||||
placeholder="Payment terms, etc.">{{ old('notes', $invoiceSettings->default_notes) }}</textarea>
|
||||
</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">
|
||||
<button type="submit" class="btn-sakura flex-1 py-3">🧾 Create Invoice</button>
|
||||
<a href="{{ route('invoices.index') }}"
|
||||
@@ -183,6 +203,7 @@
|
||||
<script>
|
||||
function invoiceForm() {
|
||||
return {
|
||||
showCustomFields: {{ old('use_custom_fields', false) ? 'true' : 'false' }},
|
||||
items: [{ description: @json($invoiceSettings->default_service_description ?? ''), quantity: 1, unit_price: 0, is_taxable: true }],
|
||||
taxRate: {{ $invoiceSettings->default_tax_rate ?? 11 }},
|
||||
get subtotal() {
|
||||
|
||||
@@ -121,27 +121,77 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 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 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 --}}
|
||||
<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>
|
||||
<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">
|
||||
|
||||
{{-- Custom Signature Text --}}
|
||||
<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">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>
|
||||
<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 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>
|
||||
|
||||
@@ -149,36 +199,6 @@
|
||||
<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="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">
|
||||
<button type="submit" class="btn-sakura flex-1 py-3">💾 Save Changes</button>
|
||||
@@ -200,6 +220,7 @@ function invoiceEditForm() {
|
||||
});
|
||||
@endphp
|
||||
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),
|
||||
taxRate: {{ $invoice->tax_rate }},
|
||||
get subtotal() {
|
||||
|
||||
@@ -373,11 +373,11 @@
|
||||
</div>
|
||||
<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;">
|
||||
{{ $invoiceSettings->signer_name ?: ($invoiceSettings->header_name ?: 'Signature') }}
|
||||
{{ $invoice->signer_name ?: ($invoiceSettings->signer_name ?: ($invoiceSettings->header_name ?: 'Signature')) }}
|
||||
</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;">
|
||||
{{ $invoiceSettings->signer_title ?: $invoiceSettings->header_title }}
|
||||
{{ $invoice->signer_title ?: ($invoiceSettings->signer_title ?: $invoiceSettings->header_title) }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -160,11 +160,11 @@
|
||||
</div>
|
||||
<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">
|
||||
{{ $invoiceSettings->signer_name ?: ($invoiceSettings->header_name ?: 'Signature') }}
|
||||
{{ $invoice->signer_name ?: ($invoiceSettings->signer_name ?: ($invoiceSettings->header_name ?: 'Signature')) }}
|
||||
</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">
|
||||
{{ $invoiceSettings->signer_title ?: $invoiceSettings->header_title }}
|
||||
{{ $invoice->signer_title ?: ($invoiceSettings->signer_title ?: $invoiceSettings->header_title) }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user