300 lines
19 KiB
PHP
300 lines
19 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', 'Invoice Settings')
|
|
@section('page-title', 'Invoice Settings 🧾')
|
|
@section('page-subtitle', 'Customize number format, header, and invoice defaults')
|
|
|
|
@section('content')
|
|
<div class="max-w-2xl mx-auto">
|
|
<div class="flex items-center gap-3 mb-5">
|
|
<a href="{{ route('invoices.index') }}"
|
|
class="btn-sakura text-sm px-3 py-2 rounded-full border border-pink-200 text-gray-500 hover:bg-pink-50">
|
|
← Back to Invoices
|
|
</a>
|
|
</div>
|
|
|
|
@if(session('success'))
|
|
<div x-data="{ show: true }" x-show="show" x-init="setTimeout(() => show = false, 4000)"
|
|
class="flex items-center justify-between gap-3 bg-green-50 border border-green-200 rounded-2xl px-4 py-3 text-sm text-green-700 mb-4">
|
|
<span>{{ session('success') }}</span>
|
|
<button @click="show = false" class="text-green-500 hover:text-green-700 text-lg leading-none">✕</button>
|
|
</div>
|
|
@endif
|
|
|
|
<form action="{{ route('invoice-settings.update') }}" method="POST" enctype="multipart/form-data" class="space-y-5">
|
|
@csrf
|
|
|
|
{{-- Number Format --}}
|
|
<div class="cat-card p-5">
|
|
<h2 class="text-sm font-bold text-gray-700 mb-4 flex items-center gap-2">🔢 Invoice Number Format</h2>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">
|
|
Prefix <span class="text-pink-400">*</span>
|
|
</label>
|
|
<input type="text" name="invoice_prefix"
|
|
value="{{ old('invoice_prefix', $settings->invoice_prefix) }}" class="input-soft text-sm"
|
|
placeholder="INV-EA" required id="prefix-input" oninput="updatePreview()">
|
|
<p class="text-xs text-gray-400 mt-1">e.g. <code>INV-EA</code> → <strong>INV-EA-001</strong></p>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">
|
|
Next Number <span class="text-pink-400">*</span>
|
|
</label>
|
|
<input type="number" name="invoice_next_number"
|
|
value="{{ old('invoice_next_number', $settings->invoice_next_number) }}"
|
|
class="input-soft text-sm" min="1" required id="number-input" oninput="updatePreview()">
|
|
<p class="text-xs text-gray-400 mt-1">Used for the next new invoice</p>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3 bg-pink-50 rounded-xl px-4 py-2.5 text-sm text-gray-600">
|
|
Preview: <strong class="text-pink-600" id="number-preview">
|
|
{{ rtrim(old('invoice_prefix', $settings->invoice_prefix), '-') }}-{{ str_pad((string) (old('invoice_next_number', $settings->invoice_next_number) ?? 1), 3, '0', STR_PAD_LEFT) }}
|
|
</strong>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Header / Branding --}}
|
|
<div class="cat-card p-5">
|
|
<h2 class="text-sm font-bold text-gray-700 mb-4 flex items-center gap-2">🎨 Invoice Header</h2>
|
|
|
|
{{-- Logo auto-upload (independent of main Save form) --}}
|
|
<div class="mb-4" x-data="{
|
|
uploading: false,
|
|
errorMsg: '',
|
|
logoUrl: '{{ $settings->header_logo ? '/storage/' . $settings->header_logo : '' }}',
|
|
|
|
async pickFile(input) {
|
|
if (!input.files.length) return;
|
|
this.uploading = true;
|
|
this.errorMsg = '';
|
|
const fd = new FormData();
|
|
fd.append('header_logo', input.files[0]);
|
|
fd.append('_token', document.querySelector('meta[name=csrf-token]').content);
|
|
try {
|
|
const res = await fetch('{{ route('invoice-settings.upload-logo', [], false) }}', { method: 'POST', body: fd });
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
this.logoUrl = data.url + '?t=' + Date.now();
|
|
} else {
|
|
this.errorMsg = data.message ?? 'Upload failed.';
|
|
}
|
|
} catch {
|
|
this.errorMsg = 'Network error. Please try again.';
|
|
}
|
|
this.uploading = false;
|
|
input.value = '';
|
|
},
|
|
|
|
async removeLogo() {
|
|
if (!confirm('Remove this logo?')) return;
|
|
const fd = new FormData();
|
|
fd.append('_token', document.querySelector('meta[name=csrf-token]').content);
|
|
fd.append('_method', 'DELETE');
|
|
await fetch('{{ route('invoice-settings.delete-logo', [], false) }}', { method: 'POST', body: fd });
|
|
this.logoUrl = '';
|
|
}
|
|
}">
|
|
|
|
{{-- Current logo preview --}}
|
|
<template x-if="logoUrl">
|
|
<div class="flex items-center gap-3 mb-3">
|
|
<img :src="logoUrl" alt="Logo"
|
|
class="h-16 w-16 object-cover rounded-2xl border border-pink-100 shrink-0">
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-xs font-semibold text-gray-600">Current Logo</p>
|
|
<p class="text-xs text-gray-400 mt-0.5">Choose a new file to replace it</p>
|
|
</div>
|
|
<button type="button" @click="removeLogo()"
|
|
class="text-xs px-3 py-1.5 rounded-full border border-red-200 text-red-400 hover:bg-red-50 shrink-0">
|
|
🗑️ Remove
|
|
</button>
|
|
</div>
|
|
</template>
|
|
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Logo / Profile Picture</label>
|
|
<div class="relative">
|
|
<input type="file" accept="image/*" @change="pickFile($event.target)" 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" :disabled="uploading">
|
|
<span x-show="uploading" class="text-xs text-pink-500 mt-1 block">Uploading…</span>
|
|
<span x-show="errorMsg" x-text="errorMsg" class="text-xs text-red-500 mt-1 block"></span>
|
|
</div>
|
|
<p class="text-xs text-gray-400 mt-1">Max 2 MB. Uploads immediately on file selection.</p>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Talent / Business Name</label>
|
|
<input type="text" name="header_name" value="{{ old('header_name', $settings->header_name) }}"
|
|
class="input-soft text-sm" placeholder="Your name or brand">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Title / Tagline</label>
|
|
<input type="text" name="header_title" value="{{ old('header_title', $settings->header_title) }}"
|
|
class="input-soft text-sm" placeholder="Content Creator · KOL">
|
|
</div>
|
|
<div x-data="{ color: '{{ old('accent_color', $settings->accent_color ?? '#FFB7C5') }}' }">
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Accent Color</label>
|
|
<div class="flex items-center gap-3 mt-1">
|
|
<input type="color" name="accent_color" x-model="color"
|
|
class="w-10 h-10 rounded-xl cursor-pointer border-2 border-pink-100 p-0.5">
|
|
</div>
|
|
<p class="text-xs text-gray-400 mt-1.5">Applied to your invoice header name and totals.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Invoice Defaults --}}
|
|
<div class="cat-card p-5">
|
|
<h2 class="text-sm font-bold text-gray-700 mb-4 flex items-center gap-2">📋 Invoice Defaults</h2>
|
|
<p class="text-xs text-gray-400 mb-4">These values will pre-fill every new invoice you create.</p>
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Default Tax Rate (%)</label>
|
|
<input type="number" name="default_tax_rate" step="0.01" min="0" max="100"
|
|
value="{{ old('default_tax_rate', $settings->default_tax_rate ?? 11) }}"
|
|
class="input-soft text-sm" placeholder="11">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Default Currency</label>
|
|
<select name="default_currency" class="input-soft text-sm">
|
|
@foreach(['IDR' => 'IDR (Rupiah)', 'USD' => 'USD (Dollar)', 'SGD' => 'SGD', 'MYR' => 'MYR (Ringgit)'] as $code => $label)
|
|
<option value="{{ $code }}" {{ old('default_currency', $settings->default_currency ?? 'IDR') === $code ? 'selected' : '' }}>
|
|
{{ $label }}
|
|
</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
<div class="sm:col-span-2">
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Default Service Description</label>
|
|
<input type="text" name="default_service_description"
|
|
value="{{ old('default_service_description', $settings->default_service_description) }}"
|
|
class="input-soft text-sm" placeholder="e.g. Instagram Feed Post — 1 carousel">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Default Bank Name</label>
|
|
<input type="text" name="default_bank_name"
|
|
value="{{ old('default_bank_name', $settings->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">Default Branch Name</label>
|
|
<input type="text" name="default_branch_name"
|
|
value="{{ old('default_branch_name', $settings->default_branch_name) }}"
|
|
class="input-soft text-sm" placeholder="KCP Sudirman">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Default Account Number</label>
|
|
<input type="text" name="default_bank_account"
|
|
value="{{ old('default_bank_account', $settings->default_bank_account) }}"
|
|
class="input-soft text-sm" placeholder="1234567890">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Default Account Holder</label>
|
|
<input type="text" name="default_bank_holder"
|
|
value="{{ old('default_bank_holder', $settings->default_bank_holder) }}"
|
|
class="input-soft text-sm" placeholder="Your full name">
|
|
</div>
|
|
<div class="sm:col-span-2">
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Default Notes / Payment Terms</label>
|
|
<textarea name="default_notes" rows="2" class="input-soft text-sm resize-none"
|
|
placeholder="e.g. Payment due within 14 days from invoice date.">{{ old('default_notes', $settings->default_notes) }}</textarea>
|
|
</div>
|
|
|
|
<div class="sm:col-span-2 mt-2 pt-4 border-t border-gray-100">
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Default Signature Image</label>
|
|
<p class="text-xs text-gray-400 mb-3">Upload your signature here so it appears automatically on all new invoices.</p>
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4">
|
|
<div>
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Signer Name</label>
|
|
<input type="text" name="signer_name" placeholder="e.g. {{ $settings->header_name ?: 'John Doe' }}" value="{{ old('signer_name', $settings->signer_name) }}" class="input-soft text-sm">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Signer Title</label>
|
|
<input type="text" name="signer_title" placeholder="e.g. {{ $settings->header_title ?: 'Manager' }}" value="{{ old('signer_title', $settings->signer_title) }}" class="input-soft text-sm">
|
|
</div>
|
|
</div>
|
|
|
|
<div x-data="{
|
|
uploading: false,
|
|
errorMsg: '',
|
|
signatureUrl: '{{ $settings->signature_image ? '/storage/' . $settings->signature_image : '' }}',
|
|
|
|
async pickFile(input) {
|
|
if (!input.files.length) return;
|
|
this.uploading = true;
|
|
this.errorMsg = '';
|
|
const fd = new FormData();
|
|
fd.append('signature_image', input.files[0]);
|
|
fd.append('_token', document.querySelector('meta[name=csrf-token]').content);
|
|
try {
|
|
const res = await fetch('{{ route('invoice-settings.upload-signature', [], false) }}', { method: 'POST', body: fd });
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
this.signatureUrl = data.url + '?t=' + Date.now();
|
|
} else {
|
|
this.errorMsg = data.message ?? 'Upload failed.';
|
|
}
|
|
} catch {
|
|
this.errorMsg = 'Network error. Please try again.';
|
|
}
|
|
this.uploading = false;
|
|
input.value = '';
|
|
},
|
|
|
|
async removeSignature() {
|
|
if (!confirm('Remove this signature?')) return;
|
|
const fd = new FormData();
|
|
fd.append('_token', document.querySelector('meta[name=csrf-token]').content);
|
|
fd.append('_method', 'DELETE');
|
|
await fetch('{{ route('invoice-settings.delete-signature', [], false) }}', { method: 'POST', body: fd });
|
|
this.signatureUrl = '';
|
|
}
|
|
}">
|
|
|
|
<template x-if="signatureUrl">
|
|
<div class="flex items-center gap-4 mb-3 p-3 bg-white rounded-xl border border-pink-100">
|
|
<img :src="signatureUrl" alt="Signature preview" class="h-16 object-contain">
|
|
<div>
|
|
<button type="button" @click="removeSignature()" 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>
|
|
</template>
|
|
|
|
<div class="relative">
|
|
<input type="file" accept="image/*" @change="pickFile($event.target)" 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" :disabled="uploading">
|
|
<span x-show="uploading" class="text-xs text-pink-500 mt-1 block">Uploading…</span>
|
|
<span x-show="errorMsg" x-text="errorMsg" class="text-xs text-red-500 mt-1 block"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" class="btn-sakura w-full py-3">💾 Save Settings</button>
|
|
</form>
|
|
</div>
|
|
|
|
@push('scripts')
|
|
<script>
|
|
function updatePreview() {
|
|
const prefix = document.getElementById('prefix-input').value.replace(/-+$/, '') || 'INV';
|
|
const num = parseInt(document.getElementById('number-input').value) || 1;
|
|
document.getElementById('number-preview').textContent = prefix + '-' + String(num).padStart(3, '0');
|
|
}
|
|
</script>
|
|
@endpush
|
|
@endsection |