301 lines
14 KiB
Plaintext
301 lines
14 KiB
Plaintext
@{
|
|
Layout = "~/Views/Admin/Transport/SpjDriverUpst/Shared/_Layout.cshtml";
|
|
ViewData["Title"] = "Batal Penjemputan";
|
|
}
|
|
|
|
<div class="w-full lg:max-w-sm mx-auto bg-gray-50 min-h-screen">
|
|
|
|
<div class="bg-upst text-white px-6 pt-8 pb-16 rounded-b-[40px] shadow-lg relative">
|
|
<div class="flex items-center justify-between relative z-10">
|
|
<a href="@Url.Action("Index", "Home")" class="w-10 h-10 flex items-center justify-center bg-white/10 rounded-xl backdrop-blur-md">
|
|
<i class="w-5 h-5" data-lucide="chevron-left"></i>
|
|
</a>
|
|
<h1 class="text-lg font-black uppercase tracking-tight">Batal Angkut</h1>
|
|
<div class="w-10"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="px-4 pt-4">
|
|
<div class="bg-white rounded-2xl p-4 shadow-sm border border-gray-100 mb-4">
|
|
<div class="flex items-center gap-3 mb-3">
|
|
<div class="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center">
|
|
<i class="w-5 h-5 text-gray-600" data-lucide="map-pin"></i>
|
|
</div>
|
|
<div>
|
|
<h3 class="font-bold text-gray-900">CV Tri Berkah Sejahtera</h3>
|
|
<p class="text-xs text-gray-500">Lokasi yang dibatalkan</p>
|
|
</div>
|
|
</div>
|
|
<p class="text-sm text-gray-700 leading-relaxed">
|
|
Kp. Pertanian II Rt.004 Rw.001 Kel. Klender Kec, Duren Sawit, Kota Adm. Jakarta Timur 13470
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="px-4 pb-6">
|
|
<div class="bg-white rounded-2xl p-5 shadow-sm border border-gray-100">
|
|
<div class="flex items-center gap-3 mb-4">
|
|
<div class="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center">
|
|
<i class="w-5 h-5 text-gray-600" data-lucide="file-text"></i>
|
|
</div>
|
|
<div>
|
|
<h2 class="text-lg font-bold text-gray-900">Form Pembatalan</h2>
|
|
<p class="text-xs text-gray-500">Berikan keterangan</p>
|
|
</div>
|
|
</div>
|
|
|
|
<form asp-action="Batal" method="post" class="space-y-5">
|
|
<!-- Alasan Pembatalan -->
|
|
<div>
|
|
<label class="block text-sm font-semibold text-gray-700 mb-3">Alasan Pembatalan</label>
|
|
<textarea name="AlasanPembatalan"
|
|
class="w-full rounded-xl text-sm border border-gray-300 focus:border-red-500 focus:ring-2 focus:ring-red-200 p-4 text-gray-700 resize-none transition-all duration-200"
|
|
rows="5"
|
|
placeholder="Jelaskan alasan pembatalan penjemputan..."
|
|
required></textarea>
|
|
<div class="text-red-500 text-sm mt-2" id="validation-message" style="display: none;">
|
|
Harap isi alasan pembatalan
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Upload Foto Bukti -->
|
|
<div>
|
|
<label class="block text-sm font-semibold text-gray-700 mb-3">Foto Bukti Pembatalan</label>
|
|
<input type="file"
|
|
name="FotoBukti"
|
|
id="fotoBukti"
|
|
accept="image/*"
|
|
multiple
|
|
class="block w-full text-sm text-gray-700 border border-gray-300 rounded-xl p-2 file:mr-3 file:rounded-lg file:border-0 file:bg-red-500 file:px-4 file:py-2 file:text-xs file:font-bold file:text-white hover:file:bg-red-600 transition-all duration-200" />
|
|
<p class="text-xs text-gray-500 mt-2">Upload foto sebagai bukti pembatalan
|
|
<div id="preview-container" class="mt-3 space-y-2"></div>
|
|
</div>
|
|
|
|
<div class="flex gap-3 pt-4">
|
|
<a href="@Url.Action("Index", "Home")"
|
|
class="flex-1 bg-gray-200 text-gray-700 font-semibold py-3 rounded-xl text-center hover:bg-gray-300 transition-colors">
|
|
Batal
|
|
</a>
|
|
<button type="submit"
|
|
class="flex-1 bg-upst text-white font-semibold py-3 rounded-xl hover:from-gray-600 hover:to-gray-700 transition-all duration-200 shadow-lg">
|
|
Konfirmasi
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- Bottom Navigation -->
|
|
<partial name="~/Views/Admin/Transport/SpjDriverUpst/Shared/Components/_Navigation.cshtml" />
|
|
</div>
|
|
|
|
<register-block dynamic-section="scripts" key="jsDetailBatal">
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const alasanTextarea = document.querySelector('textarea[name="AlasanPembatalan"]');
|
|
const form = document.querySelector('form');
|
|
const validationMessage = document.getElementById('validation-message');
|
|
const fotoBuktiInput = document.getElementById('fotoBukti');
|
|
const previewContainer = document.getElementById('preview-container');
|
|
|
|
// Fungsi watermark untuk foto pembatalan
|
|
async function applyWatermark(file, photoNumber) {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
const img = new Image();
|
|
img.onload = function() {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = img.width;
|
|
canvas.height = img.height;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
ctx.drawImage(img, 0, 0);
|
|
|
|
const now = new Date();
|
|
const days = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];
|
|
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'];
|
|
|
|
const dayName = days[now.getDay()];
|
|
const date = now.getDate().toString().padStart(2, '0');
|
|
const month = months[now.getMonth()];
|
|
const year = now.getFullYear();
|
|
const hours = now.getHours().toString().padStart(2, '0');
|
|
const minutes = now.getMinutes().toString().padStart(2, '0');
|
|
const seconds = now.getSeconds().toString().padStart(2, '0');
|
|
|
|
const timestamp = `${dayName}, ${date} ${month} ${year} • ${hours}:${minutes}:${seconds}`;
|
|
|
|
const baseFontSize = Math.max(16, Math.min(img.width, img.height) * 0.025);
|
|
const fontFamily = "'Montserrat', 'Segoe UI', 'Roboto', sans-serif";
|
|
|
|
const lines = [
|
|
{ text: `FOTO PEMBATALAN #${photoNumber}`, size: baseFontSize * 1.1, weight: '900', color: '#EF4444' },
|
|
{ text: 'SPJ/07-2025/PKM/000476', size: baseFontSize * 0.9, weight: '700', color: '#FFFFFF' },
|
|
{ text: timestamp, size: baseFontSize * 0.75, weight: '500', color: '#E2E8F0' }
|
|
];
|
|
|
|
const paddingX = baseFontSize * 1.2;
|
|
const paddingY = baseFontSize * 1.0;
|
|
const lineGap = baseFontSize * 0.4;
|
|
|
|
let maxWidth = 0;
|
|
let totalHeight = 0;
|
|
|
|
lines.forEach(line => {
|
|
ctx.font = `${line.weight} ${line.size}px ${fontFamily}`;
|
|
const metrics = ctx.measureText(line.text);
|
|
maxWidth = Math.max(maxWidth, metrics.width);
|
|
totalHeight += line.size + lineGap;
|
|
});
|
|
totalHeight -= lineGap;
|
|
|
|
const margin = baseFontSize * 1.5;
|
|
const boxWidth = maxWidth + (paddingX * 2);
|
|
const boxHeight = totalHeight + (paddingY * 2);
|
|
const boxX = img.width - boxWidth - margin;
|
|
const boxY = img.height - boxHeight - margin;
|
|
|
|
ctx.save();
|
|
|
|
ctx.beginPath();
|
|
if (ctx.roundRect) {
|
|
ctx.roundRect(boxX, boxY, boxWidth, boxHeight, baseFontSize * 0.8);
|
|
} else {
|
|
ctx.rect(boxX, boxY, boxWidth, boxHeight);
|
|
}
|
|
ctx.fillStyle = 'rgba(15, 23, 42, 0.85)';
|
|
ctx.fill();
|
|
|
|
// Garis aksen merah untuk pembatalan
|
|
ctx.beginPath();
|
|
const accentWidth = baseFontSize * 0.3;
|
|
if (ctx.roundRect) {
|
|
ctx.roundRect(boxX + boxWidth - accentWidth, boxY, accentWidth, boxHeight, [0, baseFontSize * 0.8, baseFontSize * 0.8, 0]);
|
|
} else {
|
|
ctx.rect(boxX + boxWidth - accentWidth, boxY, accentWidth, boxHeight);
|
|
}
|
|
ctx.fillStyle = '#EF4444';
|
|
ctx.fill();
|
|
|
|
ctx.restore();
|
|
|
|
ctx.shadowColor = 'rgba(0, 0, 0, 0.6)';
|
|
ctx.shadowBlur = 4;
|
|
ctx.shadowOffsetX = 1;
|
|
ctx.shadowOffsetY = 1;
|
|
ctx.textAlign = 'right';
|
|
ctx.textBaseline = 'top';
|
|
|
|
let currentY = boxY + paddingY;
|
|
const textRightLimit = boxX + boxWidth - paddingX - accentWidth;
|
|
|
|
lines.forEach(line => {
|
|
ctx.font = `${line.weight} ${line.size}px ${fontFamily}`;
|
|
ctx.fillStyle = line.color;
|
|
ctx.fillText(line.text, textRightLimit, currentY);
|
|
currentY += line.size + lineGap;
|
|
});
|
|
|
|
ctx.shadowColor = 'transparent';
|
|
|
|
canvas.toBlob(function(blob) {
|
|
const watermarkedFile = new File([blob], file.name, {
|
|
type: 'image/jpeg',
|
|
lastModified: Date.now()
|
|
});
|
|
resolve(watermarkedFile);
|
|
}, 'image/jpeg', 0.95);
|
|
};
|
|
img.onerror = reject;
|
|
img.src = e.target.result;
|
|
};
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|
|
|
|
// Preview foto dengan watermark
|
|
fotoBuktiInput.addEventListener('change', async function() {
|
|
previewContainer.innerHTML = '';
|
|
|
|
if (this.files.length > 5) {
|
|
alert('Maksimal 5 foto yang dapat diupload');
|
|
this.value = '';
|
|
return;
|
|
}
|
|
|
|
// Tampilkan loading
|
|
previewContainer.innerHTML = '<div class="text-center py-4"><div class="inline-block animate-spin rounded-full h-8 w-8 border-4 border-gray-300 border-t-red-500"></div><p class="text-sm text-gray-600 mt-2">Memproses foto...</p></div>';
|
|
|
|
const watermarkedFiles = [];
|
|
for (let i = 0; i < this.files.length; i++) {
|
|
const file = this.files[i];
|
|
try {
|
|
const watermarkedFile = await applyWatermark(file, i + 1);
|
|
watermarkedFiles.push(watermarkedFile);
|
|
} catch (error) {
|
|
console.error('Error applying watermark:', error);
|
|
watermarkedFiles.push(file);
|
|
}
|
|
}
|
|
|
|
// Hapus loading dan tampilkan preview
|
|
previewContainer.innerHTML = '';
|
|
|
|
watermarkedFiles.forEach((file, index) => {
|
|
const reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
const previewItem = document.createElement('div');
|
|
previewItem.className = 'rounded-xl border border-red-200 overflow-hidden bg-black shadow-sm hover:shadow-md transition-shadow';
|
|
previewItem.innerHTML = `
|
|
<div class="h-40 bg-black/80">
|
|
<img src="${e.target.result}" alt="Preview ${index + 1}" class="w-full h-full object-contain" />
|
|
</div>
|
|
<div class="px-3 py-2 bg-white">
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<div class="flex-shrink-0 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center">
|
|
<span class="text-white text-xs font-bold">${index + 1}</span>
|
|
</div>
|
|
<p class="text-xs font-semibold text-gray-700 truncate flex-1">${file.name}</p>
|
|
</div>
|
|
<div class="flex items-center justify-between text-[10px] text-gray-500">
|
|
<span>${(file.size / (1024 * 1024)).toFixed(2)} MB</span>
|
|
<span class="text-red-600 font-semibold">✓ Watermark</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
previewContainer.appendChild(previewItem);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
});
|
|
|
|
// Update file input dengan watermarked files
|
|
const dataTransfer = new DataTransfer();
|
|
watermarkedFiles.forEach(file => dataTransfer.items.add(file));
|
|
this.files = dataTransfer.files;
|
|
});
|
|
|
|
form.addEventListener('submit', function(e) {
|
|
if (!alasanTextarea.value.trim()) {
|
|
e.preventDefault();
|
|
validationMessage.style.display = 'block';
|
|
alasanTextarea.focus();
|
|
alasanTextarea.style.borderColor = '#ef4444';
|
|
return false;
|
|
}
|
|
|
|
validationMessage.style.display = 'none';
|
|
return true;
|
|
});
|
|
|
|
alasanTextarea.addEventListener('input', function() {
|
|
if (this.value.trim()) {
|
|
validationMessage.style.display = 'none';
|
|
this.style.borderColor = '';
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
</register-block> |