refactor: improve fetch request handling, update invoice PDF layout, and adjust docker-compose configuration

This commit is contained in:
I-Fun
2026-04-05 16:25:58 +07:00
parent ed9e7dc0fd
commit d6b10f725b
7 changed files with 78 additions and 33 deletions

View File

@@ -1,11 +1,11 @@
APP_NAME=Laravel APP_NAME="KOL-Manager"
APP_ENV=local APP_ENV=local
APP_KEY= APP_KEY=
APP_DEBUG=true APP_DEBUG=true
APP_URL=http://localhost APP_URL=http://localhost
# ── App Gate Password ───────────────────────────────────────────────────────── # ── App Gate Password ─────────────────────────────────────────────────────────
APP_PASSWORD= APP_PASSWORD=admin123
APP_LOCALE=en APP_LOCALE=en
APP_FALLBACK_LOCALE=en APP_FALLBACK_LOCALE=en

View File

@@ -92,12 +92,14 @@ RUN mkdir -p \
storage/framework/cache \ storage/framework/cache \
storage/framework/sessions \ storage/framework/sessions \
storage/framework/views \ storage/framework/views \
storage \
bootstrap/cache \ bootstrap/cache \
database \ database \
&& chown -R www-data:www-data \ && chown -R www-data:www-data \
storage \ storage \
bootstrap/cache \ bootstrap/cache \
database \ database \
/var/lib/nginx \
&& chmod -R 775 \ && chmod -R 775 \
storage \ storage \
bootstrap/cache \ bootstrap/cache \

View File

@@ -7,8 +7,8 @@ services:
container_name: kol-manager container_name: kol-manager
restart: unless-stopped restart: unless-stopped
ports: # ports:
- "${APP_PORT:-8080}:80" # - "${APP_PORT:-8080}:80"
# Mount .env.docker as the runtime .env # Mount .env.docker as the runtime .env
env_file: env_file:
@@ -22,7 +22,7 @@ services:
- kol_storage:/var/www/html/storage - kol_storage:/var/www/html/storage
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/up"] test: [ "CMD", "curl", "-f", "http://localhost/up" ]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3

View File

@@ -18,9 +18,22 @@
const fd = new FormData(); const fd = new FormData();
fd.append('avatar', input.files[0]); fd.append('avatar', input.files[0]);
fd.append('_token', document.querySelector('meta[name=csrf-token]').content); fd.append('_token', document.querySelector('meta[name=csrf-token]').content);
const res = await fetch('{{ route('clients.upload-avatar', $client) }}', { method: 'POST', body: fd }); try {
const res = await fetch('{{ route('clients.upload-avatar', $client->id, false) }}', {
method: 'POST',
body: fd,
headers: { 'Accept': 'application/json' }
});
const data = await res.json(); const data = await res.json();
if (res.ok) this.avatarUrl = data.url + '?t=' + Date.now(); if (res.ok) {
this.avatarUrl = data.url + '?t=' + Date.now();
} else {
alert(data.message || 'Upload failed');
}
} catch (err) {
console.error('Avatar upload error:', err);
alert('Network Error. Could not upload.');
}
this.uploading = false; this.uploading = false;
input.value = ''; input.value = '';
}, },
@@ -29,8 +42,16 @@
const fd = new FormData(); const fd = new FormData();
fd.append('_token', document.querySelector('meta[name=csrf-token]').content); fd.append('_token', document.querySelector('meta[name=csrf-token]').content);
fd.append('_method', 'DELETE'); fd.append('_method', 'DELETE');
await fetch('{{ route('clients.delete-avatar', $client) }}', { method: 'POST', body: fd }); try {
await fetch('{{ route('clients.delete-avatar', $client->id, false) }}', {
method: 'POST',
body: fd,
headers: { 'Accept': 'application/json' }
});
this.avatarUrl = ''; this.avatarUrl = '';
} catch (err) {
console.error(err);
}
} }
}"> }">
<div class="flex items-start gap-4"> <div class="flex items-start gap-4">

View File

@@ -303,8 +303,8 @@
<thead> <thead>
<tr> <tr>
<td style="width:50%;">Description</td> <td style="width:50%;">Description</td>
<td class="td-center" style="width:10%;">Qty</td>
<td class="td-right" style="width:20%;">Unit Price</td> <td class="td-right" style="width:20%;">Unit Price</td>
<td class="td-center" style="width:10%;">Qty</td>
<td class="td-right" style="width:20%;">Total</td> <td class="td-right" style="width:20%;">Total</td>
</tr> </tr>
</thead> </thead>
@@ -318,8 +318,8 @@
style="margin-left:6px; font-size:10px; background:#F3F4F6; color:#6B7280; padding:2px 8px; border-radius:10px; font-weight:600;">Non-Taxable</span> style="margin-left:6px; font-size:10px; background:#F3F4F6; color:#6B7280; padding:2px 8px; border-radius:10px; font-weight:600;">Non-Taxable</span>
@endif @endif
</td> </td>
<td class="td-center">{{ $item->quantity }}</td>
<td class="td-right">Rp {{ number_format($item->unit_price, 0, ',', '.') }}</td> <td class="td-right">Rp {{ number_format($item->unit_price, 0, ',', '.') }}</td>
<td class="td-center">{{ $item->quantity }}</td>
<td class="td-right" style="font-weight:600;">Rp {{ number_format($item->total, 0, ',', '.') }}</td> <td class="td-right" style="font-weight:600;">Rp {{ number_format($item->total, 0, ',', '.') }}</td>
</tr> </tr>
@endforeach @endforeach
@@ -361,17 +361,18 @@
</div> </div>
@endif @endif
<div style="margin-top: 40px;">
@if($invoice->notes) @if($invoice->notes)
<div class="notes" style="width: 50%; float: left; margin-top: 10px;">{{ $invoice->notes }}</div> <div class="notes" style="margin-bottom: 24px;">{{ $invoice->notes }}</div>
@endif @endif
<div style="margin-top: 40px;">
@if($invoice->signature_image && file_exists(storage_path('app/public/' . $invoice->signature_image))) @if($invoice->signature_image && file_exists(storage_path('app/public/' . $invoice->signature_image)))
<div style="width: 200px; float: right; text-align: center;"> <div style="width: 200px; float: right; text-align: center;">
<div style="font-size: 11px; color: #374151; margin-bottom: 12px;"> <div style="font-size: 11px; color: #374151; margin-bottom: 12px;">
Jakarta, {{ $invoice->issue_date->format('d F Y') }} Jakarta, {{ $invoice->issue_date->format('d F Y') }}
</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;">
{{ $invoice->signer_name ?: ($invoiceSettings->signer_name ?: ($invoiceSettings->header_name ?: 'Signature')) }} {{ $invoice->signer_name ?: ($invoiceSettings->signer_name ?: ($invoiceSettings->header_name ?: 'Signature')) }}
</div> </div>
@@ -386,7 +387,7 @@
<div style="clear: both;"></div> <div style="clear: both;"></div>
</div> </div>
<div class="footer">Thank you for the collaboration!</div> <!-- <div class="footer">Thank you for the collaboration!</div> -->
</body> </body>

View File

@@ -72,14 +72,21 @@
fd.append('header_logo', input.files[0]); fd.append('header_logo', input.files[0]);
fd.append('_token', document.querySelector('meta[name=csrf-token]').content); fd.append('_token', document.querySelector('meta[name=csrf-token]').content);
try { try {
const res = await fetch('{{ route('invoice-settings.upload-logo', [], false) }}', { method: 'POST', body: fd }); const res = await fetch('{{ route('invoice-settings.upload-logo', [], false) }}', {
method: 'POST',
body: fd,
headers: {
'Accept': 'application/json'
}
});
const data = await res.json(); const data = await res.json();
if (res.ok) { if (res.ok) {
this.logoUrl = data.url + '?t=' + Date.now(); this.logoUrl = data.url + '?t=' + Date.now();
} else { } else {
this.errorMsg = data.message ?? 'Upload failed.'; this.errorMsg = data.message ?? 'Upload failed.';
} }
} catch { } catch (err) {
console.error(err);
this.errorMsg = 'Network error. Please try again.'; this.errorMsg = 'Network error. Please try again.';
} }
this.uploading = false; this.uploading = false;
@@ -91,7 +98,11 @@
const fd = new FormData(); const fd = new FormData();
fd.append('_token', document.querySelector('meta[name=csrf-token]').content); fd.append('_token', document.querySelector('meta[name=csrf-token]').content);
fd.append('_method', 'DELETE'); fd.append('_method', 'DELETE');
await fetch('{{ route('invoice-settings.delete-logo', [], false) }}', { method: 'POST', body: fd }); await fetch('{{ route('invoice-settings.delete-logo', [], false) }}', {
method: 'POST',
body: fd,
headers: { 'Accept': 'application/json' }
});
this.logoUrl = ''; this.logoUrl = '';
} }
}"> }">
@@ -234,14 +245,21 @@
fd.append('signature_image', input.files[0]); fd.append('signature_image', input.files[0]);
fd.append('_token', document.querySelector('meta[name=csrf-token]').content); fd.append('_token', document.querySelector('meta[name=csrf-token]').content);
try { try {
const res = await fetch('{{ route('invoice-settings.upload-signature', [], false) }}', { method: 'POST', body: fd }); const res = await fetch('{{ route('invoice-settings.upload-signature', [], false) }}', {
method: 'POST',
body: fd,
headers: {
'Accept': 'application/json'
}
});
const data = await res.json(); const data = await res.json();
if (res.ok) { if (res.ok) {
this.signatureUrl = data.url + '?t=' + Date.now(); this.signatureUrl = data.url + '?t=' + Date.now();
} else { } else {
this.errorMsg = data.message ?? 'Upload failed.'; this.errorMsg = data.message ?? 'Upload failed.';
} }
} catch { } catch (err) {
console.error(err);
this.errorMsg = 'Network error. Please try again.'; this.errorMsg = 'Network error. Please try again.';
} }
this.uploading = false; this.uploading = false;
@@ -253,7 +271,11 @@
const fd = new FormData(); const fd = new FormData();
fd.append('_token', document.querySelector('meta[name=csrf-token]').content); fd.append('_token', document.querySelector('meta[name=csrf-token]').content);
fd.append('_method', 'DELETE'); fd.append('_method', 'DELETE');
await fetch('{{ route('invoice-settings.delete-signature', [], false) }}', { method: 'POST', body: fd }); await fetch('{{ route('invoice-settings.delete-signature', [], false) }}', {
method: 'POST',
body: fd,
headers: { 'Accept': 'application/json' }
});
this.signatureUrl = ''; this.signatureUrl = '';
} }
}"> }">

View File

@@ -90,8 +90,8 @@
<thead> <thead>
<tr class="text-xs text-gray-500 uppercase" style="background:{{ $tint }}"> <tr class="text-xs text-gray-500 uppercase" style="background:{{ $tint }}">
<th class="text-left py-3 px-4 rounded-l-2xl">Description</th> <th class="text-left py-3 px-4 rounded-l-2xl">Description</th>
<th class="text-center py-3 px-4">Qty</th>
<th class="text-right py-3 px-4">Unit Price</th> <th class="text-right py-3 px-4">Unit Price</th>
<th class="text-center py-3 px-4">Qty</th>
<th class="text-right py-3 px-4 rounded-r-2xl">Total</th> <th class="text-right py-3 px-4 rounded-r-2xl">Total</th>
</tr> </tr>
</thead> </thead>
@@ -105,9 +105,9 @@
class="ml-1.5 text-xs bg-gray-100 text-gray-500 px-2 py-0.5 rounded-full font-medium">Non-Taxable</span> class="ml-1.5 text-xs bg-gray-100 text-gray-500 px-2 py-0.5 rounded-full font-medium">Non-Taxable</span>
@endif @endif
</td> </td>
<td class="py-3 px-4 text-sm text-center text-gray-600">{{ $item->quantity }}</td>
<td class="py-3 px-4 text-sm text-right text-gray-600">Rp <td class="py-3 px-4 text-sm text-right text-gray-600">Rp
{{ number_format($item->unit_price, 0, ',', '.') }}</td> {{ number_format($item->unit_price, 0, ',', '.') }}</td>
<td class="py-3 px-4 text-sm text-center text-gray-600">{{ $item->quantity }}</td>
<td class="py-3 px-4 text-sm text-right font-semibold text-gray-800">Rp <td class="py-3 px-4 text-sm text-right font-semibold text-gray-800">Rp
{{ number_format($item->total, 0, ',', '.') }}</td> {{ number_format($item->total, 0, ',', '.') }}</td>
</tr> </tr>
@@ -137,7 +137,7 @@
{{-- Bank Info --}} {{-- Bank Info --}}
@if($invoice->bank_name) @if($invoice->bank_name)
<div class="rounded-2xl p-4 text-sm" style="background:{{ $tint }}"> <div class="rounded-2xl p-4 text-sm mb-4" style="background:{{ $tint }}">
<div class="font-semibold text-gray-700 mb-2">💳 Payment Info</div> <div class="font-semibold text-gray-700 mb-2">💳 Payment Info</div>
<div class="text-gray-600"> <div class="text-gray-600">
{{ $invoice->bank_name }} {{ $invoice->bank_name }}
@@ -147,12 +147,11 @@
</div> </div>
@endif @endif
<div class="flex flex-col sm:flex-row justify-between items-start mt-10 gap-6">
<div class="flex-1 text-left">
@if($invoice->notes) @if($invoice->notes)
<div class="text-sm text-gray-500 italic">{{ $invoice->notes }}</div> <div class="text-sm text-gray-500 italic mb-8">{{ $invoice->notes }}</div>
@endif @endif
</div>
<div class="flex justify-end mt-10">
@if($invoice->signature_image) @if($invoice->signature_image)
<div class="shrink-0 text-center w-full sm:w-auto"> <div class="shrink-0 text-center w-full sm:w-auto">
<div class="text-sm text-gray-500 mb-3"> <div class="text-sm text-gray-500 mb-3">