chore: cleanup project structure and update readme for beta release

This commit is contained in:
2025-12-23 04:59:21 +07:00
parent 1640ced748
commit 10a00bac0e
122 changed files with 8320 additions and 661 deletions

View File

@@ -0,0 +1,116 @@
@extends('layouts.app')
@section('content')
<div class="mx-auto max-w-(--breakpoint-2xl) p-4 md:p-6">
<!-- Breadcrumb -->
<div class="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 class="text-title-md2 font-semibold text-black dark:text-white">
Inbox / Messages
</h2>
<nav>
<ol class="flex items-center gap-2">
<li>
<a class="font-medium text-gray-500 hover:text-brand-500 dark:text-gray-400 dark:hover:text-brand-500"
href="{{ route('dashboard') }}">
Dashboard /
</a>
</li>
<li class="font-medium text-brand-500">Inbox</li>
</ol>
</nav>
</div>
<!-- Inbox Layout -->
<div class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<div class="max-w-full overflow-x-auto custom-scrollbar">
<table class="w-full">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr class="border-b border-gray-100 dark:border-gray-800">
<th class="px-5 py-3 text-left">
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">Sender</p>
</th>
<th class="px-5 py-3 text-left">
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">Category & Subject</p>
</th>
<th class="px-5 py-3 text-left">
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">Date</p>
</th>
<th class="px-5 py-3 text-right">
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">Actions</p>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
@forelse ($submissions as $msg)
<tr class="group hover:bg-gray-50 dark:hover:bg-white/[0.02] {{ !$msg->is_read ? 'bg-brand-50/30 dark:bg-brand-500/5' : '' }}">
<td class="px-5 py-4">
<div class="flex items-center gap-3">
<div class="relative">
<div class="w-10 h-10 rounded-full bg-brand-100 dark:bg-brand-500/10 flex items-center justify-center text-brand-600 font-bold text-sm">
{{ substr($msg->name, 0, 1) }}
</div>
@if(!$msg->is_read)
<span class="absolute top-0 right-0 h-3 w-3 rounded-full bg-brand-500 border-2 border-white dark:border-gray-900"></span>
@endif
</div>
<div>
<span class="block font-medium text-gray-800 text-theme-sm dark:text-white/90">
{{ $msg->name }}
</span>
<span class="block text-gray-500 text-theme-xs dark:text-gray-400">
{{ $msg->email }}
</span>
</div>
</div>
</td>
<td class="px-5 py-4">
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold uppercase mb-1 {{ $msg->category == 'Legal Inquiry' ? 'bg-purple-100 text-purple-700 dark:bg-purple-500/20' : 'bg-gray-100 text-gray-600 dark:bg-gray-700' }}">
{{ $msg->category }}
</span>
<p class="text-gray-800 dark:text-white/80 text-theme-sm font-medium line-clamp-1">
{{ $msg->subject }}
</p>
</td>
<td class="px-5 py-4">
<p class="text-gray-500 text-theme-xs dark:text-gray-400 whitespace-nowrap">
{{ $msg->created_at->format('M d, Y') }}
<span class="block opacity-60">{{ $msg->created_at->format('H:i') }}</span>
</p>
</td>
<td class="px-5 py-4 text-right">
<div class="flex items-center justify-end gap-2">
<a href="{{ route('admin.contacts.show', $msg->id) }}" class="p-2 text-gray-400 hover:text-brand-500 dark:hover:text-white transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
</a>
<form action="{{ route('admin.contacts.destroy', $msg->id) }}" method="POST" onsubmit="return confirm('Delete this message?')">
@csrf
@method('DELETE')
<button type="submit" class="p-2 text-gray-400 hover:text-red-500 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
</button>
</form>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="4" class="px-5 py-10 text-center">
<div class="flex flex-col items-center">
<svg class="w-12 h-12 text-gray-300 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>
<p class="text-gray-500 font-medium">No messages in your inbox yet.</p>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($submissions->hasPages())
<div class="px-5 py-4 border-t border-gray-100 dark:border-gray-800">
{{ $submissions->links() }}
</div>
@endif
</div>
</div>
@endsection

View File

@@ -0,0 +1,115 @@
@extends('layouts.app')
@section('content')
<div class="mx-auto max-w-(--breakpoint-2xl) p-4 md:p-6">
<!-- Breadcrumb -->
<div class="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 class="text-title-md2 font-semibold text-black dark:text-white">
Message Details
</h2>
<nav>
<ol class="flex items-center gap-2">
<li>
<a class="font-medium text-gray-500 hover:text-brand-500 dark:text-gray-400 dark:hover:text-brand-500"
href="{{ route('admin.contacts.index') }}">
Inbox /
</a>
</li>
<li class="font-medium text-brand-500">View Message</li>
</ol>
</nav>
</div>
<div class="bg-white rounded-xl border border-gray-200 shadow-sm dark:bg-white/[0.03] dark:border-gray-800 overflow-hidden">
<!-- Message Header -->
<div class="border-b border-gray-100 dark:border-gray-800 p-6 sm:p-8">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div class="flex items-center gap-4">
<div class="w-14 h-14 rounded-full bg-brand-500/10 flex items-center justify-center text-brand-600 font-bold text-xl">
{{ substr($contactSubmission->name, 0, 1) }}
</div>
<div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white leading-tight">
{{ $contactSubmission->name }}
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ $contactSubmission->email }}
</p>
</div>
</div>
<div class="text-right">
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold uppercase {{ $contactSubmission->category == 'Legal Inquiry' ? 'bg-purple-100 text-purple-700 dark:bg-purple-500/20' : 'bg-brand-50 text-brand-700 dark:bg-brand-500/20' }}">
{{ $contactSubmission->category }}
</span>
<p class="mt-2 text-xs text-gray-400 font-medium">
Received on {{ $contactSubmission->created_at->format('M d, Y \a\t H:i') }}
</p>
</div>
</div>
</div>
<!-- Message Body -->
<div class="p-6 sm:p-8 bg-gray-50/30 dark:bg-transparent">
<h4 class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-4">Subject</h4>
<p class="text-lg font-semibold text-gray-900 dark:text-white mb-8">
{{ $contactSubmission->subject }}
</p>
<h4 class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-4">Message</h4>
<div class="prose prose-gray dark:prose-invert max-w-none bg-white dark:bg-gray-800/20 p-6 rounded-2xl border border-gray-100 dark:border-gray-800 mb-10">
{!! nl2br(e($contactSubmission->message)) !!}
</div>
<!-- Quick Reply Form -->
<div class="mt-12 pt-10 border-t border-gray-100 dark:border-gray-800">
<h4 class="text-lg font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-brand-500"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
Quick Reply via Portal
</h4>
<form action="{{ route('admin.contacts.reply', $contactSubmission->id) }}" method="POST" class="space-y-4">
@csrf
<div>
<label class="block text-xs font-bold text-gray-400 uppercase mb-2">Subject</label>
<input type="text" name="subject" value="Re: {{ $contactSubmission->subject }}" required
class="w-full rounded-xl border-gray-200 bg-white px-4 py-3 text-gray-900 transition focus:border-brand-500 focus:ring-brand-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white">
</div>
<div>
<label class="block text-xs font-bold text-gray-400 uppercase mb-2">Message Body</label>
<textarea name="message" rows="6" required placeholder="Type your response here..."
class="w-full rounded-xl border-gray-200 bg-white px-4 py-3 text-gray-900 transition focus:border-brand-500 focus:ring-brand-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white"></textarea>
</div>
<div class="flex items-center justify-between pt-2">
<p class="text-[10px] text-gray-400 italic">
* Sending from: <span class="font-bold text-brand-500">support@lab.dyzulk.com</span>
</p>
<button type="submit" class="px-8 py-3 bg-brand-500 text-white rounded-xl font-bold hover:bg-brand-600 transition-all shadow-lg shadow-brand-500/20">
Send Reply Now
</button>
</div>
</form>
</div>
</div>
<!-- Footer Actions -->
<div class="p-6 border-t border-gray-100 dark:border-gray-800 flex items-center justify-between bg-gray-50 dark:bg-transparent">
<div class="flex items-center gap-4">
<a href="mailto:{{ $contactSubmission->email }}?subject=Re: {{ $contactSubmission->subject }}"
class="text-sm font-bold text-gray-500 hover:text-brand-500 transition-colors flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
External Email App
</a>
</div>
<form action="{{ route('admin.contacts.destroy', $contactSubmission->id) }}" method="POST" onsubmit="return confirm('Delete this message permanently?')">
@csrf
@method('DELETE')
<button type="submit" class="text-red-500 font-bold hover:underline">
Delete Message
</button>
</form>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,211 @@
@extends('layouts.app')
@section('content')
<div class="mx-auto max-w-(--breakpoint-2xl) p-4 md:p-6" x-data="{
preview: false,
content: @js($legalPage->currentRevision->content ?? ''),
currentVersion: @js($legalPage->currentRevision->version ?? '1.0.0'),
selectedVersionType: 'patch',
customVersion: '',
updateExisting: false,
get suggestions() {
let parts = this.currentVersion.split('.').map(n => parseInt(n) || 0);
while(parts.length < 3) parts.push(0);
return {
major: (parts[0] + 1) + '.0.0',
minor: parts[0] + '.' + (parts[1] + 1) + '.0',
patch: parts[0] + '.' + parts[1] + '.' + (parts[2] + 1)
};
},
get finalVersion() {
if (this.updateExisting) return this.currentVersion;
if (this.selectedVersionType === 'custom') return this.customVersion;
return this.suggestions[this.selectedVersionType];
},
markdownToHtml(text) {
if (!text) return '';
return text
.replace(/^# (.*$)/gim, '<h1 class=\'text-2xl font-bold mb-4\'>$1</h1>')
.replace(/^## (.*$)/gim, '<h2 class=\'text-xl font-bold mb-3\'>$1</h2>')
.replace(/^### (.*$)/gim, '<h3 class=\'text-lg font-bold mb-2\'>$1</h3>')
.replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
.replace(/\*(.*)\*/gim, '<em>$1</em>')
.replace(/\n/gim, '<br>');
}
}">
<!-- Breadcrumb -->
<div class="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 class="text-title-md2 font-semibold text-black dark:text-white">
Edit: {{ $legalPage->title }}
</h2>
<nav>
<ol class="flex items-center gap-2">
<li>
<a class="font-medium text-gray-500 hover:text-brand-500 dark:text-gray-400 dark:hover:text-brand-500"
href="{{ route('admin.legal-pages.index') }}">
Legal Pages /
</a>
</li>
<li class="font-medium text-brand-500">Edit</li>
</ol>
</nav>
</div>
<div class="rounded-xl border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-800 dark:bg-white/[0.03]">
<form action="{{ route('admin.legal-pages.update', $legalPage->id) }}" method="POST">
@csrf
@method('PUT')
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<!-- Left Sidebar (Meta Information) -->
<div class="lg:col-span-1 border-r border-gray-100 dark:border-gray-800 pr-0 lg:pr-6">
<!-- Toggle Section -->
<div class="mb-8 p-4 rounded-xl bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-700">
<label class="flex items-center justify-between cursor-pointer">
<div>
<span class="block text-sm font-bold text-gray-800 dark:text-gray-200">Minor Correction</span>
<span class="block text-xs text-gray-400">Fixed typo or small tweaks?</span>
</div>
<div class="relative inline-block w-10 h-6">
<input type="checkbox" name="update_existing" value="true" x-model="updateExisting" class="sr-only peer">
<div class="w-full h-full bg-gray-300 dark:bg-gray-700 rounded-full peer peer-checked:bg-brand-500 transition-all after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:after:translate-x-4"></div>
</div>
</label>
<template x-if="updateExisting">
<p class="mt-3 text-[10px] leading-tight text-brand-600 dark:text-brand-400 font-medium italic">
* "Minor Correction" mode active. The system will update the existing record without creating a new revision, and the version will remain v<span x-text="currentVersion"></span>.
</p>
</template>
</div>
<div class="mb-6" :class="updateExisting ? 'opacity-40 grayscale pointer-events-none' : ''">
<label class="mb-3 block text-sm font-medium text-black dark:text-white">
Version Selection
</label>
<input type="hidden" name="version" :value="finalVersion">
<div class="grid grid-cols-1 gap-3">
<!-- Major -->
<button type="button" @click="selectedVersionType = 'major'"
:class="selectedVersionType === 'major' ? 'border-brand-500 bg-brand-50 dark:bg-brand-500/10' : 'border-gray-200 dark:border-gray-700'"
class="flex items-center justify-between rounded-xl border-2 p-3 text-left transition-all">
<div>
<p class="text-xs font-bold text-brand-600 dark:text-brand-400">MAJOR UPDATE</p>
<p class="text-sm font-semibold text-gray-800 dark:text-gray-200" x-text="'v' + suggestions.major"></p>
</div>
<div x-show="selectedVersionType === 'major'" class="text-brand-500">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
</div>
</button>
<!-- Minor -->
<button type="button" @click="selectedVersionType = 'minor'"
:class="selectedVersionType === 'minor' ? 'border-brand-500 bg-brand-50 dark:bg-brand-500/10' : 'border-gray-200 dark:border-gray-700'"
class="flex items-center justify-between rounded-xl border-2 p-3 text-left transition-all">
<div>
<p class="text-xs font-bold text-gray-400">MINOR UPDATE</p>
<p class="text-sm font-semibold text-gray-800 dark:text-gray-200" x-text="'v' + suggestions.minor"></p>
</div>
<div x-show="selectedVersionType === 'minor'" class="text-brand-500">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
</div>
</button>
<!-- Patch -->
<button type="button" @click="selectedVersionType = 'patch'"
:class="selectedVersionType === 'patch' ? 'border-brand-500 bg-brand-50 dark:bg-brand-500/10' : 'border-gray-200 dark:border-gray-700'"
class="flex items-center justify-between rounded-xl border-2 p-3 text-left transition-all">
<div>
<p class="text-xs font-bold text-gray-400">PATCH / FIX</p>
<p class="text-sm font-semibold text-gray-800 dark:text-gray-200" x-text="'v' + suggestions.patch"></p>
</div>
<div x-show="selectedVersionType === 'patch'" class="text-brand-500">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
</div>
</button>
<!-- Custom Toggle -->
<button type="button" @click="selectedVersionType = 'custom'"
:class="selectedVersionType === 'custom' ? 'border-brand-500 bg-brand-50 dark:bg-brand-500/10' : 'border-gray-200 dark:border-gray-700'"
class="flex items-center justify-between rounded-xl border-2 p-3 text-left transition-all">
<p class="text-xs font-bold text-gray-400 uppercase tracking-wider">Custom Version</p>
</button>
<div x-show="selectedVersionType === 'custom'" x-transition class="mt-2">
<input type="text" x-model="customVersion" placeholder="e.g 2.1.3"
class="w-full rounded-lg border-[1.5px] border-gray-200 bg-transparent px-4 py-2 text-sm text-black outline-none transition focus:border-brand-500 dark:border-gray-700 dark:bg-gray-900/50 dark:text-white" />
</div>
</div>
<p class="mt-4 text-xs text-gray-400 text-center">Current active: <span class="font-bold" x-text="'v' + currentVersion"></span></p>
</div>
<div class="mb-5">
<label class="mb-3 block text-sm font-medium text-black dark:text-white">
Change Log (Small note for internal audit)
</label>
<textarea name="change_log" rows="4"
class="w-full rounded-lg border-[1.5px] border-gray-200 bg-transparent px-4 py-2 text-black outline-none transition focus:border-brand-500 active:border-brand-500 disabled:cursor-default disabled:bg-gray-100 dark:border-gray-700 dark:bg-gray-900/50 dark:text-white @error('change_log') border-error-500 @enderror"
placeholder="What changed in this version?">{{ old('change_log') }}</textarea>
@error('change_log')
<p class="mt-1 text-xs text-error-500">{{ $message }}</p>
@enderror
</div>
<div class="mt-10">
<button type="submit" class="flex w-full justify-center rounded-lg bg-brand-500 px-6 py-3 font-medium text-white hover:bg-opacity-90 transition shadow-lg shadow-brand-500/20">
<span x-text="updateExisting ? 'Save Changes' : 'Save New Revision'"></span>
</button>
</div>
</div>
<!-- Right Main Content (Markdown Editor) -->
<div class="lg:col-span-2">
<div class="mb-4 flex items-center justify-between">
<label class="block text-sm font-medium text-black dark:text-white">
Content (Markdown)
</label>
<div class="flex rounded-lg bg-gray-100 p-1 dark:bg-gray-800">
<button type="button" @click="preview = false"
:class="!preview ? 'bg-white dark:bg-gray-700 text-brand-500 shadow-sm' : 'text-gray-500'"
class="px-4 py-1.5 text-xs font-medium rounded-md transition-all">
Editor
</button>
<button type="button" @click="preview = true"
:class="preview ? 'bg-white dark:bg-gray-700 text-brand-500 shadow-sm' : 'text-gray-500'"
class="px-4 py-1.5 text-xs font-medium rounded-md transition-all">
Preview
</button>
</div>
</div>
<div x-show="!preview">
<textarea name="content" x-model="content" rows="20"
class="w-full rounded-lg border-[1.5px] border-gray-200 bg-transparent px-4 py-4 text-black font-mono text-sm outline-none transition focus:border-brand-500 active:border-brand-500 disabled:cursor-default disabled:bg-gray-100 dark:border-gray-700 dark:bg-gray-900/50 dark:text-white @error('content') border-error-500 @enderror">{{ old('content', $legalPage->currentRevision->content ?? '') }}</textarea>
@error('content')
<p class="mt-1 text-xs text-error-500">{{ $message }}</p>
@enderror
</div>
<div x-show="preview" class="min-h-[465px] rounded-lg border border-gray-200 dark:border-gray-700 p-6 bg-gray-50 dark:bg-gray-900/30 overflow-y-auto">
<div class="prose prose-gray dark:prose-invert max-w-none" x-html="markdownToHtml(content)">
</div>
</div>
<p class="mt-4 text-xs text-gray-500 text-center">
TIP: Markdown is converted to high-quality typography on the public site.
</p>
</div>
</div>
</form>
</div>
</div>
@endsection

View File

@@ -0,0 +1,116 @@
@extends('layouts.app')
@section('content')
<div class="mx-auto max-w-(--breakpoint-2xl) p-4 md:p-6">
<!-- Breadcrumb -->
<div class="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 class="text-title-md2 font-semibold text-black dark:text-white">
Legal Pages Management
</h2>
<nav>
<ol class="flex items-center gap-2">
<li>
<a class="font-medium text-gray-500 hover:text-brand-500 dark:text-gray-400 dark:hover:text-brand-500"
href="{{ route('dashboard') }}">
Dashboard /
</a>
</li>
<li class="font-medium text-brand-500">Legal Pages</li>
</ol>
</nav>
</div>
<!-- Alert -->
@if (session('success'))
<div class="mb-6 flex w-full rounded-lg border-l-6 border-success-500 bg-success-500/10 px-7 py-4 shadow-md dark:bg-[#1B2B20] md:p-6">
<div class="mr-5 flex h-9 w-9 items-center justify-center rounded-lg bg-success-500">
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.2984 0.826822L15.2867 0.811822C14.7264 0.257759 13.8182 0.253452 13.2543 0.811822L13.2506 0.815572L6.10729 7.95892L2.74759 4.59922L2.74392 4.59554C2.18124 4.03717 1.27298 4.03717 0.710351 4.59554C0.148964 5.1524 0.148964 6.05622 0.710351 6.61308L0.714024 6.61676L5.08385 10.9866L5.08752 10.9903C5.64617 11.5443 6.55445 11.5486 7.11834 10.9903L7.12201 10.9866L15.2911 2.81754C15.8525 2.26067 15.8525 1.35685 15.2911 0.800041L15.2984 0.826822Z" fill="white" />
</svg>
</div>
<div class="w-full">
<h5 class="mb-2 text-lg font-semibold text-success-800 dark:text-[#34D399]">
Successfully
</h5>
<p class="text-sm text-success-700 dark:text-[#34D399]">
{{ session('success') }}
</p>
</div>
</div>
@endif
<!-- Table Section -->
<div class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<div class="max-w-full overflow-x-auto custom-scrollbar">
<table class="w-full">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr class="border-b border-gray-100 dark:border-gray-800">
<th class="px-5 py-3 text-left sm:px-6">
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">
Page Title
</p>
</th>
<th class="px-5 py-3 text-left sm:px-6">
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">
Slug
</p>
</th>
<th class="px-5 py-3 text-left sm:px-6">
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">
Current Version
</p>
</th>
<th class="px-5 py-3 text-left sm:px-6">
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">
Last Updated
</p>
</th>
<th class="px-5 py-3 text-left sm:px-6">
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">
Actions
</p>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
@foreach ($pages as $page)
<tr class="hover:bg-gray-50 dark:hover:bg-white/[0.02]">
<td class="px-5 py-4 sm:px-6">
<span class="block font-medium text-gray-800 text-theme-sm dark:text-white/90">
{{ $page->title }}
</span>
</td>
<td class="px-5 py-4 sm:px-6">
<span class="text-gray-500 text-theme-sm dark:text-gray-400">
/legal/{{ $page->slug }}
</span>
</td>
<td class="px-5 py-4 sm:px-6">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700 dark:bg-blue-500/15 dark:text-blue-500">
v{{ $page->currentRevision->version ?? 'N/A' }}
</span>
</td>
<td class="px-5 py-4 sm:px-6">
<p class="text-gray-500 text-theme-sm dark:text-gray-400">
{{ $page->currentRevision ? $page->currentRevision->updated_at->format('M d, Y') : 'N/A' }}
</p>
</td>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center gap-3">
<a href="{{ route('admin.legal-pages.edit', $page->id) }}" class="text-brand-500 hover:text-brand-600 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path></svg>
</a>
<a href="{{ route('legal.show', $page->slug) }}" target="_blank" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
</a>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,168 @@
@extends('layouts.app')
@section('content')
<div class="mx-auto max-w-(--breakpoint-2xl) p-4 md:p-6">
<!-- Breadcrumb -->
<div class="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 class="text-title-md2 font-semibold text-black dark:text-white">
SMTP Tester
</h2>
<nav>
<ol class="flex items-center gap-2">
<li>
<a class="font-medium text-gray-500 hover:text-brand-500 dark:text-gray-400 dark:hover:text-brand-500"
href="{{ route('dashboard') }}">
Dashboard /
</a>
</li>
<li class="font-medium text-brand-500">SMTP Tester</li>
</ol>
</nav>
</div>
<div class="grid grid-cols-1 gap-9 sm:grid-cols-2">
<!-- Tester Form -->
<div class="flex flex-col gap-9">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-white/[0.03]">
<div class="border-b border-gray-200 px-6 py-4 dark:border-gray-800">
<h3 class="font-semibold text-gray-900 dark:text-white">
Run Connection Test
</h3>
</div>
<form action="{{ route('admin.smtp-tester.send') }}" method="POST" class="p-6">
@csrf
<div class="mb-4">
<label class="mb-2.5 block font-medium text-black dark:text-white">
Select Mailer Configuration
</label>
<div class="relative z-20 bg-transparent dark:bg-form-input">
<select name="mailer" id="mailerSelect" class="relative z-20 w-full appearance-none rounded border border-stroke bg-transparent px-5 py-3 outline-none transition focus:border-brand-500 active:border-brand-500 dark:border-gray-700 dark:bg-gray-800 dark:focus:border-brand-500">
@foreach($configs as $key => $config)
<option value="{{ $key }}" {{ old('mailer') == $key ? 'selected' : '' }}>
{{ $config['name'] }} ({{ $config['host'] }}:{{ $config['port'] }})
</option>
@endforeach
</select>
<span class="absolute right-4 top-1/2 z-30 -translate-y-1/2">
<svg class="fill-current" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.10186 11.1044C5.97864 11.2384 5.91703 11.3857 5.91703 11.5457C5.91703 11.7214 5.97864 11.8726 6.10186 11.9991L11.5597 17.5108C11.6967 17.6492 11.8526 17.7184 12.0274 17.7184C12.2022 17.7184 12.3582 17.6492 12.4951 17.5108L17.8981 11.9991C18.0214 11.8726 18.083 11.7214 18.083 11.5457C18.083 11.3857 18.0214 11.2384 17.8981 11.1044C17.7612 10.9571 17.6052 10.8834 17.4304 10.8834C17.2556 10.8834 17.0997 10.9571 16.9628 11.1044L12.0274 16.1265L7.03714 11.1044C6.90022 10.9571 6.74426 10.8834 6.56948 10.8834C6.39469 10.8834 6.23873 10.9571 6.10186 11.1044Z" fill="currentColor"/>
</svg>
</span>
</div>
</div>
<div class="mb-4">
<label class="mb-2.5 block font-medium text-black dark:text-white">
Target Email Address
</label>
<input type="email" name="email" required placeholder="Enter your email to receive test..." value="{{ auth()->user()->email }}"
class="w-full rounded border border-stroke bg-transparent px-5 py-3 outline-none transition focus:border-brand-500 active:border-brand-500 dark:border-gray-700 dark:bg-gray-800 dark:focus:border-brand-500" />
<p class="mt-1 text-xs text-gray-500">
We will send a raw test email to this address.
</p>
<div class="mb-4">
<label class="mb-2.5 block font-medium text-black dark:text-white">
Test Mode
</label>
<div class="flex gap-4">
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="mode" value="raw" checked class="text-brand-500 focus:ring-brand-500">
<span class="text-gray-900 dark:text-white">Raw Text (Connection Check)</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="mode" value="mailable" class="text-brand-500 focus:ring-brand-500">
<span class="text-gray-900 dark:text-white">ContactReply Mailable (Template Check)</span>
</label>
</div>
</div>
<button type="submit" class="flex w-full justify-center rounded bg-brand-500 p-3 font-medium text-gray hover:bg-opacity-90">
Send Test Email
</button>
</form>
</div>
@if(session('success'))
<div class="rounded-xl border border-green-200 bg-green-50 p-4 dark:border-green-800 dark:bg-green-900/10">
<div class="flex gap-3">
<div class="text-green-500">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>
</div>
<div>
<h4 class="font-bold text-green-900 dark:text-green-100">Test Successful</h4>
<p class="text-sm text-green-700 dark:text-green-300">{{ session('success') }}</p>
</div>
</div>
</div>
@endif
@if(session('error'))
<div class="rounded-xl border border-red-200 bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/10">
<div class="flex gap-3">
<div class="text-red-500">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>
</div>
<div>
<h4 class="font-bold text-red-900 dark:text-red-100">Connection Failed</h4>
<p class="text-sm text-red-700 dark:text-red-300 break-all">{{ session('error') }}</p>
<p class="mt-2 text-xs text-red-600 dark:text-red-400">Please check your .env configuration and ensure your SMTP server is accessible.</p>
</div>
</div>
</div>
@endif
</div>
<!-- Configuration Details -->
<div class="flex flex-col gap-9">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-white/[0.03]">
<div class="border-b border-gray-200 px-6 py-4 dark:border-gray-800">
<h3 class="font-semibold text-gray-900 dark:text-white">
Current Configuration (Read-Only)
</h3>
</div>
<div class="p-6">
<div class="space-y-6">
@foreach($configs as $key => $config)
<div class="p-4 rounded-lg bg-gray-50 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-700">
<h4 class="font-bold text-brand-500 mb-3 uppercase text-xs tracking-wider">
{{ $config['name'] }}
</h4>
<dl class="grid grid-cols-1 gap-x-4 gap-y-4 sm:grid-cols-2">
<div class="sm:col-span-1">
<dt class="text-xs font-medium text-gray-500">Host</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $config['host'] ?? 'N/A' }}</dd>
</div>
<div class="sm:col-span-1">
<dt class="text-xs font-medium text-gray-500">Port</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $config['port'] ?? 'N/A' }}</dd>
</div>
<div class="sm:col-span-1">
<dt class="text-xs font-medium text-gray-500">Username</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white text-ellipsis overflow-hidden">{{ $config['username'] ?? 'N/A' }}</dd>
</div>
<div class="sm:col-span-1">
<dt class="text-xs font-medium text-gray-500">Encryption</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white uppercase">{{ $config['encryption'] ?? 'None' }}</dd>
</div>
<div class="sm:col-span-2">
<dt class="text-xs font-medium text-gray-500">From Address</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $config['from'] ?? 'N/A' }}</dd>
</div>
</dl>
</div>
@endforeach
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Simple script to update expected 'From' display based on selection if needed,
// but the backend handles the actual sending.
</script>
@endsection

View File

@@ -0,0 +1,110 @@
@extends('layouts.app')
@section('content')
<div class="flex flex-col gap-4 mb-6 sm:flex-row sm:items-end sm:justify-between">
<div>
<h2 class="text-xl font-semibold text-gray-800 dark:text-white/90">
Ticket Management
</h2>
<nav class="mt-1">
<ol class="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
<li>
<a href="{{ route('dashboard') }}" class="inline-flex items-center gap-1.5 hover:text-brand-500 transition">
Admin
</a>
</li>
<li>
<svg class="stroke-current opacity-60" width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.75 2.5L6.25 5L3.75 7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</li>
<li class="font-medium text-gray-800 dark:text-white/90">Tickets</li>
</ol>
</nav>
</div>
</div>
<x-common.component-card :title="$title">
<!-- Filters -->
<div class="mb-6 flex flex-col md:flex-row gap-4 justify-between">
<div class="flex gap-2">
<a href="{{ request()->fullUrlWithQuery(['status' => 'all']) }}" class="px-3 py-1.5 text-sm font-medium rounded-lg {{ request('status') == 'all' || !request('status') ? 'bg-brand-50 text-brand-700 dark:bg-brand-900/20 dark:text-brand-300' : 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800' }}">All</a>
<a href="{{ request()->fullUrlWithQuery(['status' => 'open']) }}" class="px-3 py-1.5 text-sm font-medium rounded-lg {{ request('status') == 'open' ? 'bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-300' : 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800' }}">Open</a>
<a href="{{ request()->fullUrlWithQuery(['status' => 'answered']) }}" class="px-3 py-1.5 text-sm font-medium rounded-lg {{ request('status') == 'answered' ? 'bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-300' : 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800' }}">Answered</a>
<a href="{{ request()->fullUrlWithQuery(['status' => 'closed']) }}" class="px-3 py-1.5 text-sm font-medium rounded-lg {{ request('status') == 'closed' ? 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300' : 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800' }}">Closed</a>
</div>
<form method="GET" class="flex gap-2">
<input type="hidden" name="status" value="{{ request('status', 'all') }}">
<div class="relative">
<input type="text" name="search" value="{{ request('search') }}" placeholder="Search ID, Subject or User..." class="pl-9 pr-4 py-1.5 text-sm border border-gray-200 rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white focus:ring-brand-500 focus:border-brand-500 w-64">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
</div>
</div>
<button type="submit" class="px-3 py-1.5 text-sm font-medium text-white bg-gray-800 rounded-lg hover:bg-gray-700 dark:bg-gray-700 dark:hover:bg-gray-600">Filter</button>
</form>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-100 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-white/5">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400">Ticket Info</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400">User</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400">Priority</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400">Updated</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400">Action</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-white/10">
@forelse($tickets as $ticket)
<tr class="hover:bg-gray-50 dark:hover:bg-white/5 transition">
<td class="px-6 py-4 whitespace-nowrap">
@php
$statusClass = match($ticket->status) {
'open' => 'bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400',
'answered' => 'bg-green-50 text-green-700 dark:bg-green-500/10 dark:text-green-400',
'closed' => 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400',
default => 'bg-gray-100 text-gray-600'
};
@endphp
<span class="px-2.5 py-0.5 inline-flex items-center text-xs font-medium rounded-full {{ $statusClass }}">
{{ ucfirst($ticket->status) }}
</span>
</td>
<td class="px-6 py-4 text-sm text-gray-600 dark:text-gray-300">
<div class="font-medium text-gray-800 dark:text-white/90">#{{ $ticket->ticket_number }}</div>
<div class="truncate max-w-xs text-gray-500">{{ $ticket->subject }}</div>
<div class="text-xs text-gray-400 mt-1">{{ $ticket->category }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-300">
<div class="font-medium text-gray-800 dark:text-white">{{ $ticket->user->name }}</div>
<div class="text-xs text-gray-500">{{ $ticket->user->email }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 py-0.5 text-xs rounded bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 border border-gray-200 dark:border-gray-600">
{{ ucfirst($ticket->priority) }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ $ticket->updated_at->diffForHumans() }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a href="{{ route('admin.tickets.show', $ticket->id) }}" class="text-brand-500 hover:text-brand-600 dark:text-brand-400">Manage</a>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-6 py-10 text-center text-gray-500 dark:text-gray-400">
<p>No tickets found matching your criteria.</p>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-4">
{{ $tickets->links() }}
</div>
</x-common.component-card>
@endsection

View File

@@ -0,0 +1,317 @@
@extends('layouts.app')
@section('content')
<div class="flex flex-col gap-4 mb-6 sm:flex-row sm:items-end sm:justify-between">
<div>
<h2 class="text-xl font-semibold text-gray-800 dark:text-white/90">
Manage Ticket #{{ $ticket->ticket_number }}
</h2>
<nav class="mt-1">
<ol class="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
<li>
<a href="{{ route('dashboard') }}" class="inline-flex items-center gap-1.5 hover:text-brand-500 transition">
Admin
</a>
</li>
<li>
<svg class="stroke-current opacity-60" width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.75 2.5L6.25 5L3.75 7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</li>
<li>
<a href="{{ route('admin.tickets.index') }}" class="inline-flex items-center gap-1.5 hover:text-brand-500 transition">
Tickets
</a>
</li>
<li>
<svg class="stroke-current opacity-60" width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.75 2.5L6.25 5L3.75 7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</li>
<li class="font-medium text-gray-800 dark:text-white/90">Manage</li>
</ol>
</nav>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Main Chat Area -->
<div class="lg:col-span-2 space-y-6">
<x-common.component-card>
<x-slot:header>
<div class="mb-6 pb-6 border-b border-gray-100 dark:border-gray-700 flex justify-between items-start">
<div>
<h1 class="text-xl font-bold text-gray-800 dark:text-gray-100 mb-2">{{ $ticket->subject }}</h1>
<span class="px-2.5 py-0.5 inline-flex items-center text-xs font-medium rounded-full bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200">
{{ $ticket->category }}
</span>
</div>
</div>
</x-slot:header>
<div id="admin-ticket-chat-container" class="space-y-8">
@foreach($ticket->replies as $reply)
@php
// In Admin view: User messages on LEFT, Admin messages (ours) on RIGHT
// But since multiple admins might exist, we check if reply user is Admin Role or specific user
// Simpler: If reply->user_id == Auth::id() -> Right (It's ME)
// If reply->user->isAdmin() -> Right (It's a Colleague)
// Else (Customer) -> Left
$isStaff = $reply->user->isAdmin();
@endphp
<div class="flex {{ $isStaff ? 'justify-end' : 'justify-start' }}">
@if(!$isStaff)
<div class="flex-shrink-0 mr-3">
<div class="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-xs font-bold text-blue-600 dark:text-blue-300">
{{ substr($reply->user->name, 0, 1) }}
</div>
</div>
@endif
<div class="max-w-xl">
<div class="text-xs text-gray-500 mb-1 {{ $isStaff ? 'text-right' : 'text-left' }}">
{{ $reply->user->name }} {{ $isStaff ? '(Staff)' : '' }} {{ $reply->created_at->format('M d, Y H:i A') }}
</div>
<div class="px-4 py-3 rounded-lg {{ $isStaff ? 'bg-brand-500 text-white rounded-tr-none' : 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-tl-none' }}">
<p class="whitespace-pre-wrap text-sm">{{ $reply->message }}</p>
@if($reply->attachment_path)
<div class="mt-3 pt-3 border-t {{ $isStaff ? 'border-brand-400' : 'border-gray-200 dark:border-gray-600' }}">
<a href="{{ Storage::url($reply->attachment_path) }}" target="_blank" class="flex items-center text-xs {{ $isStaff ? 'text-brand-100 hover:text-white' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200' }}">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path></svg>
Attachment
</a>
</div>
@endif
</div>
</div>
@if($isStaff)
<div class="flex-shrink-0 ml-3">
<div class="w-8 h-8 rounded-full bg-brand-200 flex items-center justify-center text-xs font-bold text-brand-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>
</div>
</div>
@endif
</div>
@endforeach
</div>
<div class="mt-8 pt-6 border-t border-gray-100 dark:border-gray-700">
<h3 class="text-sm font-medium text-gray-900 dark:text-white mb-4">Post Staff Reply</h3>
<form id="admin-ticket-reply-form" action="{{ route('admin.tickets.reply', $ticket->id) }}" method="POST" enctype="multipart/form-data">
@csrf
<div class="mb-4">
<textarea id="reply-message" name="message" rows="4" class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-brand-500 focus:border-brand-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-brand-500 dark:focus:border-brand-500" placeholder="Type your reply here..." required></textarea>
</div>
<div class="mb-4" x-data="{ fileName: '' }">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" for="admin_attachment">Attachment (Optional)</label>
<div class="flex items-center justify-center w-full">
<label for="admin_attachment" class="flex flex-col items-center justify-center w-full h-24 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:hover:bg-gray-800 dark:bg-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-600 transition-colors">
<div class="flex flex-col items-center justify-center pt-3 pb-4 p-2 text-center" x-show="!fileName">
<svg class="w-6 h-6 mb-2 text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"/>
</svg>
<p class="mb-1 text-xs text-gray-500 dark:text-gray-400"><span class="font-semibold">Click to upload</span> or drag and drop</p>
<p class="text-xs text-xs text-gray-500 dark:text-gray-400">JPG, PNG, PDF or DOCX (MAX. 2MB)</p>
</div>
<div class="flex flex-col items-center justify-center pt-3 pb-4 p-2 text-center" x-show="fileName" style="display: none;">
<svg class="w-6 h-6 mb-2 text-brand-500 dark:text-brand-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<p class="mb-1 text-xs text-gray-700 dark:text-gray-300 truncate max-w-xs" x-text="fileName"></p>
</div>
<input id="admin_attachment" name="attachment" type="file" class="hidden" accept=".jpg,.jpeg,.png,.pdf,.doc,.docx" @change="fileName = $event.target.files[0] ? $event.target.files[0].name : ''" />
</label>
</div>
</div>
<div class="flex justify-end">
<button type="submit" id="submit-admin-reply" class="flex items-center text-white bg-brand-500 hover:bg-brand-600 focus:ring-4 focus:outline-none focus:ring-brand-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-brand-500 dark:hover:bg-brand-600 dark:focus:ring-brand-800 transition-all disabled:opacity-50">
<span id="submit-text">Send Staff Reply</span>
<svg id="submit-spinner" class="hidden animate-spin ml-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</button>
</div>
</form>
</div>
</x-common.component-card>
</div>
<!-- Sidebar Info -->
<div class="space-y-6">
<!-- Ticket Status Control -->
<x-common.component-card title="Ticket Status">
<form action="{{ route('admin.tickets.update-status', $ticket->id) }}" method="POST">
@csrf
@method('PATCH')
<div class="space-y-4">
<div>
<label class="block mb-1.5 text-xs font-medium text-gray-700 dark:text-gray-400">Status</label>
<select name="status" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-brand-500 focus:border-brand-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
<option value="open" {{ $ticket->status == 'open' ? 'selected' : '' }}>Open</option>
<option value="answered" {{ $ticket->status == 'answered' ? 'selected' : '' }}>Answered</option>
<option value="closed" {{ $ticket->status == 'closed' ? 'selected' : '' }}>Closed</option>
</select>
</div>
<div>
<label class="block mb-1.5 text-xs font-medium text-gray-700 dark:text-gray-400">Priority</label>
<select name="priority" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-brand-500 focus:border-brand-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
<option value="low" {{ $ticket->priority == 'low' ? 'selected' : '' }}>Low</option>
<option value="medium" {{ $ticket->priority == 'medium' ? 'selected' : '' }}>Medium</option>
<option value="high" {{ $ticket->priority == 'high' ? 'selected' : '' }}>High</option>
</select>
</div>
<button type="submit" class="w-full text-white bg-gray-800 hover:bg-gray-900 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-gray-700 dark:hover:bg-gray-600 dark:focus:ring-gray-800">
Update Status
</button>
</div>
</form>
</x-common.component-card>
<!-- User Info -->
<x-common.component-card title="Customer Profile">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-full bg-brand-100 flex items-center justify-center text-lg font-bold text-brand-600">
{{ substr($ticket->user->name, 0, 1) }}
</div>
<div>
<div class="font-medium text-gray-900 dark:text-white">{{ $ticket->user->name }}</div>
<div class="text-xs text-gray-500">{{ $ticket->user->email }}</div>
</div>
</div>
<div class="pt-4 border-t border-gray-100 dark:border-gray-700">
<h4 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Recent Tickets</h4>
@if($ticket->user->tickets->count() > 1)
<ul class="space-y-2">
@foreach($ticket->user->tickets as $userTicket)
@if($userTicket->id !== $ticket->id)
<li>
<a href="{{ route('admin.tickets.show', $userTicket->id) }}" class="flex items-center justify-between text-xs text-gray-600 dark:text-gray-400 hover:text-brand-500">
<span>#{{ $userTicket->ticket_number }}</span>
<span class="px-1.5 py-0.5 rounded-sm bg-gray-100 dark:bg-gray-800 text-gray-500">{{ $userTicket->status }}</span>
</a>
</li>
@endif
@endforeach
</ul>
@else
<p class="text-xs text-gray-400 italic">No other tickets.</p>
@endif
</div>
</x-common.component-card>
</div>
</div>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const ticketId = "{{ $ticket->id }}";
const currentUserId = "{{ Auth::id() }}";
const chatContainer = document.getElementById('admin-ticket-chat-container');
const replyForm = document.getElementById('admin-ticket-reply-form');
const submitBtn = document.getElementById('submit-admin-reply');
const submitText = document.getElementById('submit-text');
const submitSpinner = document.getElementById('submit-spinner');
const messageInput = document.getElementById('reply-message');
const appendMessage = (data) => {
// Check if message already exists to avoid duplicates
if (document.getElementById(`reply-${data.id}`)) return;
const isStaff = data.is_staff;
const messageHtml = `
<div id="reply-${data.id}" class="flex ${isStaff ? 'justify-end' : 'justify-start'} animate-fade-in-up mb-8 last:mb-0">
${!isStaff ? `
<div class="flex-shrink-0 mr-3">
<div class="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-xs font-bold text-blue-600 dark:text-blue-300">
${data.user_name.substring(0, 1)}
</div>
</div>
` : ''}
<div class="max-w-xl">
<div class="text-xs text-gray-500 mb-1 ${isStaff ? 'text-right' : 'text-left'}">
${data.user_name} ${isStaff ? '(Staff)' : ''} ${data.created_at}
</div>
<div class="px-4 py-3 rounded-lg ${isStaff ? 'bg-brand-500 text-white rounded-tr-none' : 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-tl-none'}">
<p class="whitespace-pre-wrap text-sm">${data.message}</p>
${data.attachment_url ? `
<div class="mt-3 pt-3 border-t ${isStaff ? 'border-brand-400' : 'border-gray-200 dark:border-gray-600'}">
<a href="${data.attachment_url}" target="_blank" class="flex items-center text-xs ${isStaff ? 'text-brand-100 hover:text-white' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'}">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path></svg>
Attachment
</a>
</div>
` : ''}
</div>
</div>
${isStaff ? `
<div class="flex-shrink-0 ml-3">
<div class="w-8 h-8 rounded-full bg-brand-200 flex items-center justify-center text-xs font-bold text-brand-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>
</div>
</div>
` : ''}
</div>
`;
chatContainer.insertAdjacentHTML('beforeend', messageHtml);
// Scroll to bottom
window.scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth'
});
};
// Listen for realtime messages
if (window.Echo) {
window.Echo.private(`ticket.${ticketId}`)
.listen('.ticket.message.sent', (e) => {
console.log('Realtime message received:', e);
appendMessage(e);
});
}
// Handle AJAX form submission
if (replyForm) {
replyForm.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(replyForm);
// UI States
submitBtn.disabled = true;
submitText.innerText = 'Sending...';
submitSpinner.classList.remove('hidden');
window.axios.post(replyForm.action, formData)
.then(response => {
if (response.data.success) {
// Append message locally immediately
appendMessage(response.data.reply);
// Reset form
replyForm.reset();
}
})
.catch(error => {
console.error('Error sending staff reply:', error);
alert('Failed to send reply. Please try again.');
})
.finally(() => {
submitBtn.disabled = false;
submitText.innerText = 'Send Staff Reply';
submitSpinner.classList.add('hidden');
});
});
}
});
</script>
<style>
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in-up {
animation: fadeInUp 0.3s ease-out forwards;
}
</style>
@endpush