Files
BetiX/resources/js/pages/settings/Kyc.vue
Dolo 0280278978
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
Initialer Laravel Commit für BetiX
2026-04-04 18:01:50 +02:00

374 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { Head, router } from '@inertiajs/vue3';
import { ref, onMounted, nextTick } from 'vue';
import UserLayout from '../../layouts/user/userlayout.vue';
type Doc = {
id: number
category: string
type: string
status: string
rejection_reason?: string | null
mime: string
size: number
created_at: string
}
defineProps<{
documents: Doc[]
accepted: Record<string, string[]>
maxUploadMb: number
}>()
const uploading = ref(false);
const error = ref<string | null>(null);
const success = ref<string | null>(null);
const category = ref<'identity'|'address'|'payment'>('identity');
const type = ref<string>('passport');
const fileInput = ref<HTMLInputElement | null>(null);
function onFileChosen(e: Event) {
const input = e.target as HTMLInputElement;
if (!input.files || input.files.length === 0) return;
upload(input.files[0]);
}
async function upload(file: File) {
uploading.value = true;
error.value = null;
success.value = null;
const form = new FormData();
form.append('category', category.value);
form.append('type', type.value);
form.append('file', file);
try {
const res = await fetch('/settings/kyc', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement)?.content || ''
},
body: form,
});
if (!res.ok) {
const j = await res.json().catch(() => ({}));
throw new Error(j.message || 'Upload failed');
}
success.value = 'Document uploaded successfully';
router.reload({ only: ['documents'] });
} catch (e: any) {
error.value = e?.message || 'Upload failed';
} finally {
uploading.value = false;
if (fileInput.value) fileInput.value.value = '';
}
}
async function removeDoc(id: number) {
if (!confirm('Delete this document? Only pending documents can be deleted.')) return;
const res = await fetch(`/settings/kyc/${id}`, {
method: 'DELETE',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement)?.content || ''
},
});
if (res.ok) {
router.reload({ only: ['documents'] });
}
}
function formatSize(bytes: number) {
if (bytes < 1024) return `${bytes} B`;
const kb = bytes/1024; if (kb < 1024) return `${kb.toFixed(1)} KB`;
const mb = kb/1024; return `${mb.toFixed(2)} MB`;
}
onMounted(() => {
nextTick(() => { if (window.lucide) window.lucide.createIcons(); });
});
</script>
<template>
<UserLayout>
<Head title="KYC Verification" />
<section class="content">
<div class="wrap">
<div class="panel main-panel">
<header class="page-head">
<div class="head-flex">
<div class="title-group">
<div class="title">KYC Protocol</div>
<p class="subtitle">Secure identity verification & document management</p>
</div>
<div class="status-indicator">
<span class="dot"></span>
<span class="status-text">System Active</span>
</div>
</div>
</header>
<div class="settings-nav">
<a href="/settings/profile" class="nav-item"><i data-lucide="user"></i> Profile</a>
<a href="/settings/kyc" class="nav-item active"><i data-lucide="file-check"></i> KYC</a>
<a href="/settings/security" class="nav-item"><i data-lucide="lock"></i> Security</a>
<a href="/settings/two-factor" class="nav-item"><i data-lucide="shield"></i> 2FA</a>
</div>
<div class="grid-layout">
<div class="left-col">
<div class="glass-card uploader-box">
<div class="form-grid">
<div class="input-group">
<label class="lbl">Category</label>
<div class="select-wrapper">
<select v-model="category">
<option value="identity">Identity</option>
<option value="address">Address</option>
<option value="payment">Payment</option>
</select>
</div>
</div>
<div class="input-group">
<label class="lbl">Document Type</label>
<div class="select-wrapper">
<select v-model="type">
<option value="passport">Passport</option>
<option value="driver_license">Driver license</option>
<option value="id_card">ID card</option>
<option value="bank_statement">Bank statement</option>
<option value="utility_bill">Utility bill</option>
<option value="online_banking">Online banking</option>
<option value="other">Other</option>
</select>
</div>
</div>
</div>
<div class="drop-zone" @click="fileInput?.click()" :class="{ 'is-uploading': uploading }">
<div class="drop-content">
<i data-lucide="shield-check" class="drop-icon"></i>
<div class="drop-text">Click or drag-and-drop file</div>
<small class="drop-info">JPG, PNG, WEBP, PDF Max {{ maxUploadMb }} MB</small>
</div>
<div v-if="uploading" class="upload-overlay">
<div class="spinner"></div>
</div>
<input ref="fileInput" type="file" class="hidden" @change="onFileChosen" :disabled="uploading" />
</div>
<div class="feedback-area">
<p v-if="error" class="msg err"><i data-lucide="alert-circle"></i> {{ error }}</p>
<p v-if="success" class="msg ok"><i data-lucide="check-circle"></i> {{ success }}</p>
</div>
</div>
<div class="document-list-container">
<div class="list-header">
<h3>Your Documents</h3>
<span class="count">{{ documents.length }} Files</span>
</div>
<div v-if="!documents.length" class="empty-state">
<i data-lucide="folder-open"></i>
<p>No documents uploaded yet</p>
</div>
<div class="docs-grid">
<div v-for="d in documents" :key="d.id" class="doc-card" :data-status="d.status">
<div class="doc-main">
<div class="doc-info">
<div class="doc-badges">
<span class="badge-cat">{{ d.category }}</span>
<span class="badge-type">{{ d.type }}</span>
</div>
<div class="doc-status">
<span class="status-dot"></span>
<span class="status-label">{{ d.status }}</span>
</div>
</div>
<div class="doc-meta">
<span>{{ d.mime.split('/')[1]?.toUpperCase() }}</span>
<span>{{ formatSize(d.size) }}</span>
<span>{{ new Date(d.created_at).toLocaleDateString() }}</span>
</div>
<div v-if="d.rejection_reason" class="rejection-box">
Reason: {{ d.rejection_reason }}
</div>
</div>
<div class="doc-actions">
<a :href="`/settings/kyc/${d.id}/download`" target="_blank" class="act-btn view">
<i data-lucide="eye"></i>
</a>
<button v-if="d.status==='pending'" @click="removeDoc(d.id)" class="act-btn delete">
<i data-lucide="trash-2"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<aside class="right-col">
<div class="glass-card side-info">
<div class="side-head">Guidelines</div>
<div class="guide-sections">
<div class="guide-item">
<div class="g-title"><i data-lucide="user"></i> Identity</div>
<ul>
<li>Passport (Full page)</li>
<li>Drivers license (Both sides)</li>
<li>ID Card (Both sides)</li>
</ul>
</div>
<div class="guide-item">
<div class="g-title"><i data-lucide="map-pin"></i> Address</div>
<ul>
<li>Bank statement (Last 3m)</li>
<li>Utility bill (Electricity/Water)</li>
</ul>
</div>
<div class="guide-item">
<div class="g-title"><i data-lucide="credit-card"></i> Payment</div>
<ul>
<li>Online banking screenshot</li>
</ul>
</div>
</div>
<div class="secure-footer">
<i data-lucide="lock" style="width:12px"></i> End-to-end Encrypted
</div>
</div>
</aside>
</div>
</div>
</div>
</section>
</UserLayout>
</template>
<style scoped>
:global(:root) {
--bg-card: #0a0a0a;
--border: #151515;
--cyan: #00f2ff;
--magenta: #ff007a;
--green: #00ff9d;
--gold: #f7931a;
--red: #ff3e3e;
}
.content { padding: 30px; animation: fade-in 0.8s cubic-bezier(0.2, 0, 0, 1); }
.wrap { max-width: 1200px; margin: 0 auto; }
.main-panel { background: var(--bg-card); border: 1px solid var(--border); border-radius: 20px; overflow: hidden; box-shadow: 0 20px 50px rgba(0,0,0,0.5); }
/* Header Styles */
.page-head { padding: 25px 30px; border-bottom: 1px solid var(--border); background: linear-gradient(to right, rgba(0,242,255,0.03), transparent); }
.head-flex { display: flex; justify-content: space-between; align-items: center; }
.title { font-size: 14px; font-weight: 900; color: #fff; letter-spacing: 3px; text-transform: uppercase; }
.subtitle { color: #555; font-size: 12px; margin-top: 4px; font-weight: 600; }
.status-indicator { display: flex; align-items: center; gap: 8px; background: rgba(0,0,0,0.3); padding: 6px 12px; border-radius: 50px; border: 1px solid #111; }
.status-indicator .dot { width: 6px; height: 6px; background: var(--green); border-radius: 50%; box-shadow: 0 0 10px var(--green); animation: pulse 2s infinite; }
.status-text { font-size: 10px; font-weight: 900; color: #666; text-transform: uppercase; letter-spacing: 1px; }
/* Navigation */
.settings-nav { display: flex; gap: 5px; padding: 15px 30px; border-bottom: 1px solid var(--border); background: rgba(0,0,0,0.2); overflow-x: auto; }
.nav-item { display: flex; align-items: center; gap: 8px; padding: 10px 16px; border-radius: 8px; font-size: 11px; font-weight: 800; color: #666; text-transform: uppercase; letter-spacing: 1px; transition: 0.2s; text-decoration: none; }
.nav-item:hover { color: #fff; background: rgba(255,255,255,0.05); }
.nav-item.active { color: var(--cyan); background: rgba(0,242,255,0.1); }
.nav-item i { width: 14px; }
/* Grid Layout */
.grid-layout { display: grid; grid-template-columns: 1fr 320px; gap: 30px; padding: 30px; }
/* Left Column / Uploader */
.left-col { display: flex; flex-direction: column; gap: 30px; animation: slide-up 0.6s cubic-bezier(0.2, 0, 0, 1) backwards; animation-delay: 0.1s; }
.glass-card { background: #050505; border: 1px solid var(--border); border-radius: 16px; padding: 20px; transition: 0.3s; }
.glass-card:hover { border-color: #222; box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 20px; }
.lbl { display: block; font-size: 10px; font-weight: 900; color: #444; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
.select-wrapper { position: relative; }
select { width: 100%; background: #000; border: 1px solid var(--border); color: #fff; padding: 12px; border-radius: 12px; font-size: 13px; appearance: none; transition: 0.3s; }
select:focus { border-color: var(--cyan); outline: none; box-shadow: 0 0 15px rgba(0,242,255,0.1); }
/* Drop Zone */
.drop-zone { border: 2px dashed #1a1a1a; border-radius: 16px; padding: 40px 20px; text-align: center; cursor: pointer; transition: 0.3s; position: relative; overflow: hidden; }
.drop-zone:hover { border-color: var(--cyan); background: rgba(0,242,255,0.02); transform: scale(1.01); }
.drop-icon { width: 32px; height: 32px; color: #222; margin: 0 auto 15px; transition: 0.3s; }
.drop-zone:hover .drop-icon { color: var(--cyan); transform: translateY(-3px); }
.drop-text { font-weight: 800; color: #eee; font-size: 14px; margin-bottom: 5px; }
.drop-info { color: #444; font-size: 11px; }
.upload-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 10; }
.spinner { width: 24px; height: 24px; border: 3px solid rgba(0,242,255,0.1); border-top-color: var(--cyan); border-radius: 50%; animation: spin 0.8s linear infinite; }
/* Documents List */
.list-header { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 15px; }
.list-header h3 { font-size: 11px; font-weight: 900; text-transform: uppercase; letter-spacing: 2px; color: #444; margin: 0; }
.list-header .count { font-size: 10px; color: #666; font-weight: 800; }
.docs-grid { display: grid; gap: 12px; }
.doc-card { display: flex; justify-content: space-between; align-items: center; background: #070707; border: 1px solid var(--border); padding: 16px; border-radius: 14px; transition: 0.3s; animation: fade-in 0.5s backwards; }
.doc-card:hover { border-color: #222; transform: translateX(5px); }
.doc-info { display: flex; align-items: center; gap: 15px; margin-bottom: 8px; }
.doc-badges { display: flex; gap: 6px; }
.badge-cat { font-size: 9px; font-weight: 900; color: var(--cyan); text-transform: uppercase; background: rgba(0,242,255,0.05); padding: 2px 8px; border-radius: 4px; }
.badge-type { font-size: 9px; font-weight: 900; color: #666; text-transform: uppercase; background: #111; padding: 2px 8px; border-radius: 4px; }
.doc-status { display: flex; align-items: center; gap: 6px; }
.status-dot { width: 6px; height: 6px; border-radius: 50%; background: #444; }
.status-label { font-size: 10px; font-weight: 900; text-transform: uppercase; color: #444; }
.doc-card[data-status="approved"] .status-dot { background: var(--green); box-shadow: 0 0 8px var(--green); }
.doc-card[data-status="approved"] .status-label { color: var(--green); }
.doc-card[data-status="pending"] .status-dot { background: var(--gold); }
.doc-card[data-status="rejected"] .status-dot { background: var(--red); }
.doc-meta { display: flex; gap: 15px; font-size: 11px; color: #333; font-weight: 700; }
.rejection-box { margin-top: 8px; font-size: 11px; color: var(--red); background: rgba(255,62,62,0.05); padding: 6px 10px; border-radius: 6px; }
.doc-actions { display: flex; gap: 8px; }
.act-btn { width: 36px; height: 36px; border-radius: 10px; display: flex; align-items: center; justify-content: center; cursor: pointer; border: 1px solid #151515; background: #000; color: #444; transition: 0.2s; }
.act-btn:hover { color: #fff; border-color: #333; }
.act-btn.delete:hover { color: var(--red); border-color: var(--red); background: rgba(255,62,62,0.05); }
/* Right Column */
.right-col { animation: slide-up 0.6s cubic-bezier(0.2, 0, 0, 1) backwards; animation-delay: 0.2s; }
.side-info { position: sticky; top: 30px; }
.side-head { font-size: 11px; font-weight: 900; text-transform: uppercase; letter-spacing: 2px; color: #fff; margin-bottom: 20px; border-left: 3px solid var(--cyan); padding-left: 12px; }
.guide-sections { display: flex; flex-direction: column; gap: 20px; }
.g-title { font-size: 10px; font-weight: 900; text-transform: uppercase; color: #555; display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.g-title i { width: 12px; }
.guide-item ul { padding-left: 20px; margin: 0; }
.guide-item li { font-size: 12px; color: #888; margin-bottom: 5px; }
.secure-footer { margin-top: 25px; padding-top: 15px; border-top: 1px solid #111; font-size: 10px; font-weight: 800; color: #333; text-transform: uppercase; display: flex; align-items: center; gap: 6px; }
/* Utility */
.empty-state { padding: 40px; text-align: center; color: #222; }
.empty-state i { width: 40px; height: 40px; margin-bottom: 10px; opacity: 0.5; }
.msg { font-size: 12px; font-weight: 800; display: flex; align-items: center; gap: 8px; margin-top: 15px; }
.msg i { width: 14px; }
.msg.err { color: var(--red); }
.msg.ok { color: var(--green); }
.hidden { display: none; }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } }
@keyframes fade-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
@keyframes slide-up { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
@media (max-width: 1000px) {
.grid-layout { grid-template-columns: 1fr; }
.side-info { position: static; }
.form-grid { grid-template-columns: 1fr; }
.settings-nav { padding: 10px 15px; }
}
</style>