refactor: improve fetch request handling, update invoice PDF layout, and adjust docker-compose configuration
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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 \
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 data = await res.json();
|
const res = await fetch('{{ route('clients.upload-avatar', $client->id, false) }}', {
|
||||||
if (res.ok) this.avatarUrl = data.url + '?t=' + Date.now();
|
method: 'POST',
|
||||||
|
body: fd,
|
||||||
|
headers: { 'Accept': 'application/json' }
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
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 {
|
||||||
this.avatarUrl = '';
|
await fetch('{{ route('clients.delete-avatar', $client->id, false) }}', {
|
||||||
|
method: 'POST',
|
||||||
|
body: fd,
|
||||||
|
headers: { 'Accept': 'application/json' }
|
||||||
|
});
|
||||||
|
this.avatarUrl = '';
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}">
|
}">
|
||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
|
|||||||
@@ -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="margin-bottom: 24px;">{{ $invoice->notes }}</div>
|
||||||
<div class="notes" style="width: 50%; float: left; margin-top: 10px;">{{ $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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 = '';
|
||||||
}
|
}
|
||||||
}">
|
}">
|
||||||
|
|||||||
@@ -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">
|
@if($invoice->notes)
|
||||||
<div class="flex-1 text-left">
|
<div class="text-sm text-gray-500 italic mb-8">{{ $invoice->notes }}</div>
|
||||||
@if($invoice->notes)
|
@endif
|
||||||
<div class="text-sm text-gray-500 italic">{{ $invoice->notes }}</div>
|
|
||||||
@endif
|
<div class="flex justify-end mt-10">
|
||||||
</div>
|
|
||||||
@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">
|
||||||
|
|||||||
Reference in New Issue
Block a user