Add customer contact inquiry system, automated receipt emails, admin communication inbox, and customer profile management
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Communication;
|
||||||
use App\Models\Order;
|
use App\Models\Order;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -25,7 +26,13 @@ class AccountController extends Controller
|
|||||||
->latest()
|
->latest()
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
return view('account.index', compact('user', 'orders'));
|
$communications = Communication::where('user_id', $user->id)
|
||||||
|
->orWhere('email', strtolower($user->email))
|
||||||
|
->with('replies')
|
||||||
|
->latest()
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('account.index', compact('user', 'orders', 'communications'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Mail\ContactInquiryReplyMail;
|
||||||
|
use App\Models\Communication;
|
||||||
|
use App\Models\CommunicationReply;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class CommunicationController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of all customer communications / inquiries.
|
||||||
|
*/
|
||||||
|
public function index(Request $request): View
|
||||||
|
{
|
||||||
|
$query = Communication::with(['user', 'order', 'replies'])->latest();
|
||||||
|
|
||||||
|
// Status filter
|
||||||
|
if ($request->filled('status') && $request->input('status') !== 'all') {
|
||||||
|
$query->where('status', $request->input('status'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search query
|
||||||
|
if ($request->filled('q')) {
|
||||||
|
$search = trim($request->input('q'));
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('name', 'like', "%{$search}%")
|
||||||
|
->orWhere('email', 'like', "%{$search}%")
|
||||||
|
->orWhere('subject', 'like', "%{$search}%")
|
||||||
|
->orWhere('message', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$communications = $query->paginate(20)->withQueryString();
|
||||||
|
|
||||||
|
$counts = [
|
||||||
|
'all' => Communication::count(),
|
||||||
|
'new' => Communication::where('status', 'new')->count(),
|
||||||
|
'in_progress' => Communication::where('status', 'in_progress')->count(),
|
||||||
|
'resolved' => Communication::where('status', 'resolved')->count(),
|
||||||
|
'closed' => Communication::where('status', 'closed')->count(),
|
||||||
|
];
|
||||||
|
|
||||||
|
return view('admin.communications.index', [
|
||||||
|
'communications' => $communications,
|
||||||
|
'counts' => $counts,
|
||||||
|
'currentStatus' => $request->input('status', 'all'),
|
||||||
|
'search' => $request->input('q', ''),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified communication thread.
|
||||||
|
*/
|
||||||
|
public function show(Communication $communication): View
|
||||||
|
{
|
||||||
|
$communication->load(['user.orders', 'order.items', 'replies.user']);
|
||||||
|
|
||||||
|
// Mark as in_progress if still new when viewed by admin
|
||||||
|
if ($communication->status === 'new') {
|
||||||
|
$communication->update(['status' => 'in_progress']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('admin.communications.show', [
|
||||||
|
'communication' => $communication,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the status and/or admin notes for this communication.
|
||||||
|
*/
|
||||||
|
public function updateStatus(Request $request, Communication $communication): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'status' => 'required|string|in:new,in_progress,resolved,closed',
|
||||||
|
'admin_notes' => 'nullable|string|max:5000',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$communication->update($validated);
|
||||||
|
|
||||||
|
return back()->with('success', 'Status van het bericht is bijgewerkt.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a reply to the customer from the admin dashboard.
|
||||||
|
*/
|
||||||
|
public function sendReply(Request $request, Communication $communication): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'message' => 'required|string|min:5|max:5000',
|
||||||
|
], [
|
||||||
|
'message.required' => 'Vul alstublieft een antwoordbericht in.',
|
||||||
|
'message.min' => 'Het antwoordbericht moet minimaal 5 tekens bevatten.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$reply = CommunicationReply::create([
|
||||||
|
'communication_id' => $communication->id,
|
||||||
|
'user_id' => Auth::id(),
|
||||||
|
'is_admin_reply' => true,
|
||||||
|
'message' => trim($validated['message']),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$communication->update([
|
||||||
|
'status' => 'resolved',
|
||||||
|
'last_replied_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Send email to customer from info@jouwdriedee.nl
|
||||||
|
try {
|
||||||
|
Mail::to($communication->email)->send(new ContactInquiryReplyMail($communication, $reply));
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error('Fout bij verzenden van admin reactie naar klant: '.$e->getMessage(), [
|
||||||
|
'communication_id' => $communication->id,
|
||||||
|
'email' => $communication->email,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('warning', 'Het antwoord is opgeslagen in de database, maar er trad een fout op bij het verzenden van de e-mail: '.$e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('success', 'Jouw antwoord is verzonden naar '.$communication->email.' en toegevoegd aan het communicatiedossier.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified communication from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(Communication $communication): RedirectResponse
|
||||||
|
{
|
||||||
|
$communication->delete();
|
||||||
|
|
||||||
|
return redirect()->route('admin.communications.index')->with('success', 'Klantbericht is verwijderd.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Communication;
|
||||||
|
use App\Models\Order;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class CustomerController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of registered customers and guest buyers.
|
||||||
|
*/
|
||||||
|
public function index(Request $request): View
|
||||||
|
{
|
||||||
|
$search = trim($request->input('q', ''));
|
||||||
|
|
||||||
|
// Query registered non-admin users
|
||||||
|
$usersQuery = User::where('is_admin', false);
|
||||||
|
if ($search !== '') {
|
||||||
|
$usersQuery->where(function ($q) use ($search) {
|
||||||
|
$q->where('name', 'like', "%{$search}%")
|
||||||
|
->orWhere('email', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$registeredUsers = $usersQuery->withCount(['orders', 'communications'])->latest()->get();
|
||||||
|
|
||||||
|
// Also discover guest customers who placed orders or sent inquiries without an account
|
||||||
|
$registeredEmails = User::pluck('email')->map(fn ($e) => strtolower($e))->toArray();
|
||||||
|
|
||||||
|
$guestOrderEmails = Order::whereNull('user_id')
|
||||||
|
->select('customer_email', 'customer_name', 'customer_phone', 'created_at')
|
||||||
|
->latest()
|
||||||
|
->get()
|
||||||
|
->groupBy(fn ($item) => strtolower($item->customer_email));
|
||||||
|
|
||||||
|
$guestInquiryEmails = Communication::whereNull('user_id')
|
||||||
|
->select('email', 'name', 'phone', 'created_at')
|
||||||
|
->latest()
|
||||||
|
->get()
|
||||||
|
->groupBy(fn ($item) => strtolower($item->email));
|
||||||
|
|
||||||
|
$guestCustomers = collect();
|
||||||
|
$allGuestEmails = $guestOrderEmails->keys()->merge($guestInquiryEmails->keys())->unique();
|
||||||
|
|
||||||
|
foreach ($allGuestEmails as $email) {
|
||||||
|
if (in_array(strtolower($email), $registeredEmails)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($search !== '' && ! str_contains(strtolower($email), strtolower($search))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$orderGroup = $guestOrderEmails->get($email, collect());
|
||||||
|
$inquiryGroup = $guestInquiryEmails->get($email, collect());
|
||||||
|
|
||||||
|
$latestOrder = $orderGroup->first();
|
||||||
|
$latestInquiry = $inquiryGroup->first();
|
||||||
|
|
||||||
|
$name = $latestOrder ? $latestOrder->customer_name : ($latestInquiry ? $latestInquiry->name : 'Gast');
|
||||||
|
$phone = $latestOrder ? $latestOrder->customer_phone : ($latestInquiry ? $latestInquiry->phone : null);
|
||||||
|
$createdAt = $latestOrder ? $latestOrder->created_at : ($latestInquiry ? $latestInquiry->created_at : now());
|
||||||
|
|
||||||
|
$guestCustomers->push((object) [
|
||||||
|
'id' => null,
|
||||||
|
'is_registered' => false,
|
||||||
|
'name' => $name,
|
||||||
|
'email' => $email,
|
||||||
|
'phone' => $phone,
|
||||||
|
'orders_count' => $orderGroup->count(),
|
||||||
|
'communications_count' => $inquiryGroup->count(),
|
||||||
|
'created_at' => $createdAt,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map registered users into unified customer object
|
||||||
|
$registeredCustomers = $registeredUsers->map(function ($u) {
|
||||||
|
return (object) [
|
||||||
|
'id' => $u->id,
|
||||||
|
'is_registered' => true,
|
||||||
|
'name' => $u->name,
|
||||||
|
'email' => $u->email,
|
||||||
|
'phone' => null,
|
||||||
|
'orders_count' => $u->orders_count,
|
||||||
|
'communications_count' => $u->communications_count,
|
||||||
|
'created_at' => $u->created_at,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$allCustomers = $registeredCustomers->concat($guestCustomers)->sortByDesc('created_at')->values();
|
||||||
|
|
||||||
|
// Manual pagination for the unified list
|
||||||
|
$perPage = 20;
|
||||||
|
$page = LengthAwarePaginator::resolveCurrentPage('page');
|
||||||
|
$paginatedCustomers = new LengthAwarePaginator(
|
||||||
|
$allCustomers->forPage($page, $perPage),
|
||||||
|
$allCustomers->count(),
|
||||||
|
$perPage,
|
||||||
|
$page,
|
||||||
|
['path' => route('admin.customers.index'), 'query' => $request->query()]
|
||||||
|
);
|
||||||
|
|
||||||
|
return view('admin.customers.index', [
|
||||||
|
'customers' => $paginatedCustomers,
|
||||||
|
'totalRegistered' => $registeredUsers->count(),
|
||||||
|
'totalGuests' => $guestCustomers->count(),
|
||||||
|
'search' => $search,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a specific customer's profile, including their full communication dossier and orders.
|
||||||
|
*/
|
||||||
|
public function show(string $identifier): View
|
||||||
|
{
|
||||||
|
$user = null;
|
||||||
|
$email = null;
|
||||||
|
|
||||||
|
if (is_numeric($identifier)) {
|
||||||
|
$user = User::find($identifier);
|
||||||
|
if ($user) {
|
||||||
|
$email = strtolower($user->email);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$email = strtolower(urldecode($identifier));
|
||||||
|
$user = User::where('email', $email)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $user && ! $email) {
|
||||||
|
abort(404, 'Klant niet gevonden.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all communications for this customer (by user_id OR by email)
|
||||||
|
$communicationsQuery = Communication::with(['replies', 'order'])->latest();
|
||||||
|
if ($user) {
|
||||||
|
$communicationsQuery->where(function ($q) use ($user, $email) {
|
||||||
|
$q->where('user_id', $user->id)
|
||||||
|
->orWhere('email', $email);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$communicationsQuery->where('email', $email);
|
||||||
|
}
|
||||||
|
$communications = $communicationsQuery->get();
|
||||||
|
|
||||||
|
// Fetch all orders for this customer (by user_id OR by email)
|
||||||
|
$ordersQuery = Order::with('items')->latest();
|
||||||
|
if ($user) {
|
||||||
|
$ordersQuery->where(function ($q) use ($user, $email) {
|
||||||
|
$q->where('user_id', $user->id)
|
||||||
|
->orWhere('customer_email', $email);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$ordersQuery->where('customer_email', $email);
|
||||||
|
}
|
||||||
|
$orders = $ordersQuery->get();
|
||||||
|
|
||||||
|
$customerName = $user ? $user->name : ($orders->first()?->customer_name ?? $communications->first()?->name ?? 'Klant');
|
||||||
|
$customerPhone = $orders->first()?->customer_phone ?? $communications->first()?->phone ?? null;
|
||||||
|
$totalSpent = $orders->sum('total');
|
||||||
|
|
||||||
|
return view('admin.customers.show', [
|
||||||
|
'user' => $user,
|
||||||
|
'email' => $email,
|
||||||
|
'customerName' => $customerName,
|
||||||
|
'customerPhone' => $customerPhone,
|
||||||
|
'totalSpent' => $totalSpent,
|
||||||
|
'communications' => $communications,
|
||||||
|
'orders' => $orders,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Mail\ContactInquiryConfirmationMail;
|
||||||
|
use App\Mail\ContactInquiryReceivedMail;
|
||||||
|
use App\Models\Communication;
|
||||||
|
use App\Models\Order;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class ContactController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show the contact / question form.
|
||||||
|
*/
|
||||||
|
public function index(): View
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
$userOrders = $user ? $user->orders()->latest()->take(10)->get() : collect();
|
||||||
|
|
||||||
|
return view('contact.index', [
|
||||||
|
'user' => $user,
|
||||||
|
'userOrders' => $userOrders,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the submission of a question / contact inquiry.
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:150',
|
||||||
|
'email' => 'required|email|max:150',
|
||||||
|
'phone' => 'nullable|string|max:40',
|
||||||
|
'subject' => 'required|string|max:200',
|
||||||
|
'order_number' => 'nullable|string|max:50',
|
||||||
|
'message' => 'required|string|min:10|max:5000',
|
||||||
|
], [
|
||||||
|
'name.required' => 'Vul alstublieft jouw naam in.',
|
||||||
|
'email.required' => 'Vul alstublieft een geldig e-mailadres in.',
|
||||||
|
'email.email' => 'Het ingevoerde e-mailadres is niet geldig.',
|
||||||
|
'subject.required' => 'Vul alstublieft het onderwerp van jouw vraag in.',
|
||||||
|
'message.required' => 'Vul alstublieft jouw bericht of vraag in.',
|
||||||
|
'message.min' => 'Het bericht moet minimaal 10 tekens bevatten.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$orderId = null;
|
||||||
|
if (! empty($validated['order_number'])) {
|
||||||
|
$cleanNumber = ltrim(trim($validated['order_number']), '#');
|
||||||
|
$foundOrder = Order::where('order_number', $cleanNumber)->first();
|
||||||
|
if ($foundOrder) {
|
||||||
|
$orderId = $foundOrder->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$communication = Communication::create([
|
||||||
|
'user_id' => Auth::id(),
|
||||||
|
'order_id' => $orderId,
|
||||||
|
'name' => trim($validated['name']),
|
||||||
|
'email' => strtolower(trim($validated['email'])),
|
||||||
|
'phone' => ! empty($validated['phone']) ? trim($validated['phone']) : null,
|
||||||
|
'subject' => trim($validated['subject']),
|
||||||
|
'message' => trim($validated['message']),
|
||||||
|
'status' => 'new',
|
||||||
|
'ip_address' => $request->ip(),
|
||||||
|
'user_agent' => substr((string) $request->userAgent(), 0, 500),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 1. Send incoming mail to webshop management (info@jouwdrieedee.nl / info@jouwdriedee.nl)
|
||||||
|
try {
|
||||||
|
$shopInbox = Setting::get('contact_email', 'info@jouwdrieedee.nl');
|
||||||
|
// Ensure delivery to both info@jouwdrieedee.nl and fallback info@jouwdriedee.nl
|
||||||
|
$recipients = array_unique([$shopInbox, 'info@jouwdrieedee.nl', 'info@jouwdriedee.nl']);
|
||||||
|
Mail::to($recipients)->send(new ContactInquiryReceivedMail($communication));
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error('Fout bij versturen van klantvraag naar webshop beheer: '.$e->getMessage(), [
|
||||||
|
'communication_id' => $communication->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Send afschrift receipt from (noreply@jouwdriedee.nl) to the customer
|
||||||
|
try {
|
||||||
|
Mail::to($communication->email)->send(new ContactInquiryConfirmationMail($communication));
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error('Fout bij versturen van afschrift naar klant: '.$e->getMessage(), [
|
||||||
|
'communication_id' => $communication->id,
|
||||||
|
'email' => $communication->email,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('contact.index')->with(
|
||||||
|
'success',
|
||||||
|
'Bedankt voor je vraag! We hebben jouw bericht ontvangen. Er is een afschrift verzonden naar '.$communication->email.'.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use App\Models\Communication;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Address;
|
||||||
|
use Illuminate\Mail\Mailables\Content;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class ContactInquiryConfirmationMail extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public function __construct(public Communication $communication) {}
|
||||||
|
|
||||||
|
public function envelope(): Envelope
|
||||||
|
{
|
||||||
|
return new Envelope(
|
||||||
|
from: new Address('noreply@jouwdriedee.nl', 'Jouwdriedee.nl'),
|
||||||
|
subject: 'Afschrift: Jouw vraag aan Jouwdriedee.nl - '.$this->communication->subject,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function content(): Content
|
||||||
|
{
|
||||||
|
return new Content(
|
||||||
|
view: 'emails.contact-inquiry-confirmation',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use App\Models\Communication;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Address;
|
||||||
|
use Illuminate\Mail\Mailables\Content;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class ContactInquiryReceivedMail extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public function __construct(public Communication $communication) {}
|
||||||
|
|
||||||
|
public function envelope(): Envelope
|
||||||
|
{
|
||||||
|
return new Envelope(
|
||||||
|
subject: '[Nieuwe Klantvraag] #'.$this->communication->id.': '.$this->communication->subject,
|
||||||
|
replyTo: [
|
||||||
|
new Address($this->communication->email, $this->communication->name),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function content(): Content
|
||||||
|
{
|
||||||
|
return new Content(
|
||||||
|
view: 'emails.contact-inquiry-received',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use App\Models\Communication;
|
||||||
|
use App\Models\CommunicationReply;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Address;
|
||||||
|
use Illuminate\Mail\Mailables\Content;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class ContactInquiryReplyMail extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public Communication $communication,
|
||||||
|
public CommunicationReply $reply
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function envelope(): Envelope
|
||||||
|
{
|
||||||
|
return new Envelope(
|
||||||
|
from: new Address('info@jouwdriedee.nl', 'Jouwdriedee.nl Klantenservice'),
|
||||||
|
replyTo: [
|
||||||
|
new Address('info@jouwdriedee.nl', 'Jouwdriedee.nl Klantenservice'),
|
||||||
|
],
|
||||||
|
subject: 'Reactie op jouw vraag: '.$this->communication->subject.' - Jouwdriedee.nl',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function content(): Content
|
||||||
|
{
|
||||||
|
return new Content(
|
||||||
|
view: 'emails.contact-inquiry-reply',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class Communication extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'order_id',
|
||||||
|
'name',
|
||||||
|
'email',
|
||||||
|
'phone',
|
||||||
|
'subject',
|
||||||
|
'message',
|
||||||
|
'status',
|
||||||
|
'admin_notes',
|
||||||
|
'ip_address',
|
||||||
|
'user_agent',
|
||||||
|
'last_replied_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'last_replied_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function order(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Order::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function replies(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(CommunicationReply::class)->oldest();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatusLabelAttribute(): string
|
||||||
|
{
|
||||||
|
return match ($this->status) {
|
||||||
|
'new' => 'Nieuw',
|
||||||
|
'in_progress' => 'In behandeling',
|
||||||
|
'resolved' => 'Beantwoord',
|
||||||
|
'closed' => 'Gesloten',
|
||||||
|
default => ucfirst($this->status),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class CommunicationReply extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'communication_id',
|
||||||
|
'user_id',
|
||||||
|
'is_admin_reply',
|
||||||
|
'message',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_admin_reply' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function communication(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Communication::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,4 +48,14 @@ class User extends Authenticatable
|
|||||||
{
|
{
|
||||||
return $this->hasMany(Order::class);
|
return $this->hasMany(Order::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Communications / questions submitted by this user.
|
||||||
|
*
|
||||||
|
* @return HasMany<Communication, $this>
|
||||||
|
*/
|
||||||
|
public function communications(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Communication::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('communications', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||||
|
$table->foreignId('order_id')->nullable()->constrained('orders')->nullOnDelete();
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('email')->index();
|
||||||
|
$table->string('phone')->nullable();
|
||||||
|
$table->string('subject');
|
||||||
|
$table->text('message');
|
||||||
|
$table->string('status')->default('new')->index(); // new, in_progress, resolved, closed
|
||||||
|
$table->text('admin_notes')->nullable();
|
||||||
|
$table->string('ip_address', 45)->nullable();
|
||||||
|
$table->text('user_agent')->nullable();
|
||||||
|
$table->timestamp('last_replied_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('communication_replies', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('communication_id')->constrained('communications')->cascadeOnDelete();
|
||||||
|
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||||
|
$table->boolean('is_admin_reply')->default(true);
|
||||||
|
$table->text('message');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('communication_replies');
|
||||||
|
Schema::dropIfExists('communications');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -56,11 +56,24 @@
|
|||||||
>
|
>
|
||||||
<i data-lucide="package" class="w-3.5 h-3.5"></i>
|
<i data-lucide="package" class="w-3.5 h-3.5"></i>
|
||||||
<span>Mijn Bestellingen</span>
|
<span>Mijn Bestellingen</span>
|
||||||
<span class="px-1.5 py-0.2 rounded-md text-[10px] {{ $tab === 'orders' ? 'bg-slate-100 text-slate-700' : 'bg-slate-200/60 text-slate-600' }}">
|
<span class="px-1.5 py-0.2 rounded-md text-[10px] bg-slate-200/60 text-slate-700">
|
||||||
{{ $orders->count() }}
|
{{ $orders->count() }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="tab = 'messages'"
|
||||||
|
:class="tab === 'messages' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900'"
|
||||||
|
class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 cursor-pointer"
|
||||||
|
>
|
||||||
|
<i data-lucide="message-square" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>Mijn Vragen</span>
|
||||||
|
<span class="px-1.5 py-0.2 rounded-md text-[10px] bg-slate-200/60 text-slate-700">
|
||||||
|
{{ $communications->count() }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="tab = 'profile'"
|
@click="tab = 'profile'"
|
||||||
@@ -153,6 +166,86 @@
|
|||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB: MIJN VRAGEN & COMMUNICATIE -->
|
||||||
|
<div x-show="tab === 'messages'" class="space-y-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-sm font-bold text-slate-900">Jouw gestelde vragen aan Jouwdriedee.nl</h2>
|
||||||
|
<p class="text-xs text-slate-500">Volg hier jouw berichten en antwoorden van onze printstudio.</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('contact.index') }}" class="px-3.5 py-1.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-white text-xs font-medium transition flex items-center space-x-1.5 shadow-xs">
|
||||||
|
<i data-lucide="plus" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>Nieuwe vraag stellen</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($communications->isEmpty())
|
||||||
|
<div class="p-10 text-center rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-2.5">
|
||||||
|
<div class="w-10 h-10 rounded-xl bg-slate-100 text-slate-400 flex items-center justify-center mx-auto">
|
||||||
|
<i data-lucide="message-square" class="w-5 h-5"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-sm font-semibold text-slate-900">Nog geen vragen gesteld</h3>
|
||||||
|
<p class="text-xs text-slate-500 max-w-xs mx-auto">
|
||||||
|
Heb je een vraag over een model, levering of materiaal? Stel hem gerust!
|
||||||
|
</p>
|
||||||
|
<div class="pt-2">
|
||||||
|
<a href="{{ route('contact.index') }}" class="inline-flex items-center space-x-1.5 px-4 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-white text-xs font-medium transition shadow-xs">
|
||||||
|
<i data-lucide="message-square" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>Vraag stellen aan de shop</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="space-y-3">
|
||||||
|
@foreach($communications as $comm)
|
||||||
|
<div class="p-5 rounded-2xl bg-white border border-slate-200/80 shadow-2xs space-y-3">
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-2.5 border-b border-slate-100 gap-2">
|
||||||
|
<div>
|
||||||
|
<span class="font-mono text-xs text-slate-400">#{{ $comm->id }}</span>
|
||||||
|
<h3 class="font-bold text-slate-900 text-xs sm:text-sm inline ml-1">{{ $comm->subject }}</h3>
|
||||||
|
<span class="text-xs text-slate-400 ml-2">• {{ $comm->created_at->format('d-m-Y om H:i') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="px-2 py-0.5 rounded-full text-[10px] font-semibold self-start sm:self-auto
|
||||||
|
@if($comm->status === 'new') bg-sky-50 text-sky-700 border border-sky-200/60
|
||||||
|
@elseif($comm->status === 'in_progress') bg-amber-50 text-amber-700 border border-amber-200/60
|
||||||
|
@elseif($comm->status === 'resolved') bg-emerald-50 text-emerald-700 border border-emerald-200/60
|
||||||
|
@else bg-slate-100 text-slate-600 border border-slate-200/60 @endif">
|
||||||
|
{{ $comm->status_label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Oorspronkelijke vraag -->
|
||||||
|
<div class="text-xs text-slate-700 leading-relaxed bg-slate-50 p-3 rounded-xl border border-slate-100">
|
||||||
|
<span class="font-semibold text-slate-900 block text-[11px] mb-1">Jouw vraag:</span>
|
||||||
|
{{ $comm->message }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Eventuele antwoorden van de shop -->
|
||||||
|
@if($comm->replies->isNotEmpty())
|
||||||
|
<div class="space-y-2 pt-1">
|
||||||
|
<span class="text-[11px] font-semibold text-emerald-700 block">Antwoord van Jouwdriedee.nl:</span>
|
||||||
|
@foreach($comm->replies as $reply)
|
||||||
|
<div class="p-3 rounded-xl bg-emerald-50/60 border border-emerald-200/70 text-xs text-slate-800 space-y-1">
|
||||||
|
<div class="flex items-center justify-between text-[10px] text-emerald-700 font-medium">
|
||||||
|
<span>Studio Specialist</span>
|
||||||
|
<span>{{ $reply->created_at->format('d-m-Y H:i') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="whitespace-pre-wrap">{{ $reply->message }}</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="text-[11px] text-slate-400 italic">
|
||||||
|
We hebben jouw vraag in behandeling. Je ontvangt zo snel mogelijk antwoord via e-mail.
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- TAB 2: PROFIEL & BEVEILIGING -->
|
<!-- TAB 2: PROFIEL & BEVEILIGING -->
|
||||||
<div x-show="tab === 'profile'" class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
<div x-show="tab === 'profile'" class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', 'Communicatie & Klantvragen | Jouwdriedee Beheer')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="space-y-6">
|
||||||
|
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-6 border-b border-slate-200/80 gap-4">
|
||||||
|
<div>
|
||||||
|
<div class="inline-flex items-center space-x-1.5 text-xs text-slate-500 font-medium mb-1">
|
||||||
|
<i data-lucide="message-square" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>Klantenservice & Inquiries</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-2xl font-bold tracking-tight text-slate-900">Communicatie ({{ $counts['all'] }})</h1>
|
||||||
|
<p class="text-xs text-slate-500 mt-1">
|
||||||
|
Overzicht van binnengekomen vragen via de webshop, afschriften en verzonden reacties.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<a href="{{ route('admin.customers.index') }}" class="px-3.5 py-2 rounded-xl bg-white hover:bg-slate-50 text-slate-700 text-xs font-medium transition flex items-center space-x-1.5 border border-slate-200/80 shadow-2xs">
|
||||||
|
<i data-lucide="users" class="w-3.5 h-3.5 text-slate-400"></i>
|
||||||
|
<span>Klantprofielen</span>
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('contact.index') }}" target="_blank" class="px-3.5 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-white text-xs font-medium transition flex items-center space-x-1.5 shadow-xs">
|
||||||
|
<i data-lucide="external-link" class="w-3.5 h-3.5 text-slate-400"></i>
|
||||||
|
<span>Open Formulier</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Flash message -->
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="p-3.5 rounded-xl bg-emerald-50 border border-emerald-200/60 text-emerald-800 text-xs flex items-center space-x-2.5">
|
||||||
|
<i data-lucide="check-circle" class="w-4 h-4 text-emerald-600 shrink-0"></i>
|
||||||
|
<span class="font-medium">{{ session('success') }}</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<!-- Filter & Search Bar -->
|
||||||
|
<div class="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
|
||||||
|
|
||||||
|
<!-- Status Tabs -->
|
||||||
|
<div class="flex items-center space-x-1.5 p-1 bg-slate-100/80 rounded-xl max-w-max text-xs font-medium overflow-x-auto">
|
||||||
|
<a href="{{ route('admin.communications.index', ['status' => 'all', 'q' => $search]) }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ $currentStatus === 'all' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900' }}">
|
||||||
|
<span>Alle</span>
|
||||||
|
<span class="px-1.5 py-0.2 rounded-md text-[10px] {{ $currentStatus === 'all' ? 'bg-slate-100 text-slate-700' : 'bg-slate-200/60 text-slate-600' }}">
|
||||||
|
{{ $counts['all'] }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ route('admin.communications.index', ['status' => 'new', 'q' => $search]) }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ $currentStatus === 'new' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900' }}">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-sky-500 {{ $counts['new'] > 0 ? 'animate-pulse' : '' }}"></span>
|
||||||
|
<span>Nieuw</span>
|
||||||
|
@if($counts['new'] > 0)
|
||||||
|
<span class="px-1.5 py-0.2 rounded-md text-[10px] bg-sky-100 text-sky-800 font-bold">
|
||||||
|
{{ $counts['new'] }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ route('admin.communications.index', ['status' => 'in_progress', 'q' => $search]) }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ $currentStatus === 'in_progress' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900' }}">
|
||||||
|
<span>In behandeling</span>
|
||||||
|
<span class="px-1.5 py-0.2 rounded-md text-[10px] bg-slate-200/60 text-slate-600">
|
||||||
|
{{ $counts['in_progress'] }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ route('admin.communications.index', ['status' => 'resolved', 'q' => $search]) }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ $currentStatus === 'resolved' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900' }}">
|
||||||
|
<span>Beantwoord</span>
|
||||||
|
<span class="px-1.5 py-0.2 rounded-md text-[10px] bg-slate-200/60 text-slate-600">
|
||||||
|
{{ $counts['resolved'] }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ route('admin.communications.index', ['status' => 'closed', 'q' => $search]) }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ $currentStatus === 'closed' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900' }}">
|
||||||
|
<span>Gesloten</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Form -->
|
||||||
|
<form action="{{ route('admin.communications.index') }}" method="GET" class="flex items-center space-x-2">
|
||||||
|
<input type="hidden" name="status" value="{{ $currentStatus }}">
|
||||||
|
<div class="relative w-full sm:w-64">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="q"
|
||||||
|
value="{{ $search }}"
|
||||||
|
placeholder="Zoek op naam, e-mail..."
|
||||||
|
class="w-full pl-8 pr-3 py-1.5 bg-white border border-slate-200/80 rounded-xl text-xs text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-slate-900 focus:border-slate-900"
|
||||||
|
>
|
||||||
|
<i data-lucide="search" class="w-3.5 h-3.5 text-slate-400 absolute left-2.5 top-1/2 -translate-y-1/2"></i>
|
||||||
|
</div>
|
||||||
|
@if($search)
|
||||||
|
<a href="{{ route('admin.communications.index', ['status' => $currentStatus]) }}" class="px-2.5 py-1.5 text-xs text-slate-500 hover:text-slate-800 transition">
|
||||||
|
Wissen
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Communications Table Card -->
|
||||||
|
<div class="rounded-2xl bg-white border border-slate-200/80 overflow-hidden shadow-2xs">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left text-xs text-slate-700">
|
||||||
|
<thead class="bg-slate-50/80 text-[11px] uppercase tracking-wider text-slate-500 font-semibold border-b border-slate-200/70">
|
||||||
|
<tr>
|
||||||
|
<th class="py-3 px-4 w-20">ID</th>
|
||||||
|
<th class="py-3 px-4 w-40">Datum & Tijd</th>
|
||||||
|
<th class="py-3 px-4">Klant</th>
|
||||||
|
<th class="py-3 px-4">Onderwerp & Vraag</th>
|
||||||
|
<th class="py-3 px-4 w-32">Status</th>
|
||||||
|
<th class="py-3 px-4 text-right w-28">Actie</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100 font-medium">
|
||||||
|
@forelse($communications as $comm)
|
||||||
|
<tr class="hover:bg-slate-50/60 transition {{ $comm->status === 'new' ? 'bg-sky-50/30 font-semibold' : '' }}">
|
||||||
|
|
||||||
|
<!-- ID -->
|
||||||
|
<td class="py-3.5 px-4 font-mono text-slate-500">
|
||||||
|
#{{ $comm->id }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Datum -->
|
||||||
|
<td class="py-3.5 px-4 text-slate-500 whitespace-nowrap">
|
||||||
|
<div>{{ $comm->created_at->format('d-m-Y') }}</div>
|
||||||
|
<div class="text-[10px] text-slate-400 font-mono">{{ $comm->created_at->format('H:i') }}</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Klant -->
|
||||||
|
<td class="py-3.5 px-4">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<a href="{{ route('admin.customers.show', $comm->user_id ?? $comm->email) }}" class="font-semibold text-slate-900 hover:underline">
|
||||||
|
{{ $comm->name }}
|
||||||
|
</a>
|
||||||
|
@if($comm->user_id)
|
||||||
|
<span class="px-1.5 py-0.2 rounded bg-slate-100 text-slate-600 text-[10px] font-normal" title="Geregistreerde klant">
|
||||||
|
Account
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<span class="px-1.5 py-0.2 rounded bg-slate-50 text-slate-400 text-[10px] font-normal" title="Gastbezoeker">
|
||||||
|
Gast
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="text-[11px] text-slate-400 font-normal">
|
||||||
|
{{ $comm->email }}
|
||||||
|
</div>
|
||||||
|
@if($comm->order)
|
||||||
|
<div class="text-[10px] text-slate-500 mt-0.5">
|
||||||
|
Betreft order:
|
||||||
|
<a href="{{ route('admin.orders.show', $comm->order) }}" class="font-mono text-indigo-600 hover:underline">
|
||||||
|
#{{ $comm->order->order_number }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Onderwerp & Snippet -->
|
||||||
|
<td class="py-3.5 px-4">
|
||||||
|
<a href="{{ route('admin.communications.show', $comm) }}" class="font-bold text-slate-900 hover:text-slate-700 block truncate max-w-md">
|
||||||
|
{{ $comm->subject }}
|
||||||
|
</a>
|
||||||
|
<p class="text-[11px] text-slate-500 font-normal line-clamp-1 max-w-md mt-0.5">
|
||||||
|
{{ $comm->message }}
|
||||||
|
</p>
|
||||||
|
@if($comm->replies->isNotEmpty())
|
||||||
|
<span class="inline-flex items-center space-x-1 text-[10px] text-emerald-700 font-semibold mt-1">
|
||||||
|
<i data-lucide="check" class="w-3 h-3"></i>
|
||||||
|
<span>{{ $comm->replies->count() }} reactie(s) verzonden</span>
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Status Badge -->
|
||||||
|
<td class="py-3.5 px-4 whitespace-nowrap">
|
||||||
|
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-semibold
|
||||||
|
@if($comm->status === 'new') bg-sky-50 text-sky-700 border border-sky-200/80
|
||||||
|
@elseif($comm->status === 'in_progress') bg-amber-50 text-amber-700 border border-amber-200/80
|
||||||
|
@elseif($comm->status === 'resolved') bg-emerald-50 text-emerald-700 border border-emerald-200/80
|
||||||
|
@else bg-slate-100 text-slate-600 border border-slate-200/60
|
||||||
|
@endif">
|
||||||
|
{{ $comm->status_label }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Actie -->
|
||||||
|
<td class="py-3.5 px-4 text-right whitespace-nowrap">
|
||||||
|
<a href="{{ route('admin.communications.show', $comm) }}" class="inline-flex items-center space-x-1 px-2.5 py-1 rounded-lg bg-slate-100 hover:bg-slate-900 text-slate-700 hover:text-white text-xs font-medium transition">
|
||||||
|
<span>Bekijken</span>
|
||||||
|
<i data-lucide="chevron-right" class="w-3 h-3"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="py-12 text-center text-slate-400">
|
||||||
|
<div class="w-10 h-10 rounded-xl bg-slate-100 flex items-center justify-center text-slate-400 mx-auto mb-2">
|
||||||
|
<i data-lucide="inbox" class="w-5 h-5"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs font-medium text-slate-600">Geen berichten gevonden</p>
|
||||||
|
<p class="text-[11px] text-slate-400 mt-0.5">Vragen van klanten via het contactformulier verschijnen hier direct.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($communications->hasPages())
|
||||||
|
<div class="p-4 border-t border-slate-100">
|
||||||
|
{{ $communications->links() }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', 'Klantvraag #' . $communication->id . ': ' . $communication->subject . ' | Jouwdriedee Beheer')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="space-y-6 max-w-6xl mx-auto">
|
||||||
|
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-6 border-b border-slate-200/80 gap-4">
|
||||||
|
<div>
|
||||||
|
<a href="{{ route('admin.communications.index') }}" class="text-xs text-slate-500 hover:text-slate-900 font-medium inline-flex items-center space-x-1 mb-1 transition">
|
||||||
|
<i data-lucide="arrow-left" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>Terug naar Communicatie overzicht</span>
|
||||||
|
</a>
|
||||||
|
<div class="flex items-center space-x-2 mt-0.5">
|
||||||
|
<span class="font-mono text-xs text-slate-400">#{{ $communication->id }}</span>
|
||||||
|
<h1 class="text-xl sm:text-2xl font-bold tracking-tight text-slate-900">{{ $communication->subject }}</h1>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-slate-500 mt-0.5">
|
||||||
|
Ontvangen op {{ $communication->created_at->format('d-m-Y om H:i') }} • Afschrift verzonden naar klant
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<!-- Delete Communication -->
|
||||||
|
<form action="{{ route('admin.communications.destroy', $communication) }}" method="POST" onsubmit="return confirm('Weet je zeker dat je dit klantbericht wilt verwijderen?');">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="px-3 py-1.5 rounded-lg text-slate-400 hover:text-rose-600 hover:bg-rose-50 border border-transparent hover:border-rose-200 text-xs font-medium transition cursor-pointer flex items-center space-x-1">
|
||||||
|
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>Verwijderen</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Flash message -->
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="p-3.5 rounded-xl bg-emerald-50 border border-emerald-200/60 text-emerald-800 text-xs flex items-center space-x-2.5">
|
||||||
|
<i data-lucide="check-circle" class="w-4 h-4 text-emerald-600 shrink-0"></i>
|
||||||
|
<span class="font-medium">{{ session('success') }}</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@if(session('warning'))
|
||||||
|
<div class="p-3.5 rounded-xl bg-amber-50 border border-amber-200/60 text-amber-800 text-xs flex items-center space-x-2.5">
|
||||||
|
<i data-lucide="alert-triangle" class="w-4 h-4 text-amber-600 shrink-0"></i>
|
||||||
|
<span class="font-medium">{{ session('warning') }}</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
|
||||||
|
|
||||||
|
<!-- Linkerkolom: Bericht Thread & Antwoordformulier -->
|
||||||
|
<div class="lg:col-span-8 space-y-6">
|
||||||
|
|
||||||
|
<!-- Oorspronkelijke vraag van de klant -->
|
||||||
|
<div class="bg-white rounded-2xl border border-slate-200/80 shadow-xs p-6 space-y-4">
|
||||||
|
<div class="flex items-center justify-between pb-3 border-b border-slate-100">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<div class="w-9 h-9 rounded-xl bg-slate-900 text-white flex items-center justify-center font-bold text-xs">
|
||||||
|
{{ strtoupper(substr($communication->name, 0, 1)) }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span class="font-bold text-slate-900 text-xs">{{ $communication->name }}</span>
|
||||||
|
<span class="text-[10px] text-slate-400"><{{ $communication->email }}></span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[11px] text-slate-400">Oorspronkelijk bericht</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-[11px] text-slate-400 font-mono">
|
||||||
|
{{ $communication->created_at->format('d-m-Y H:i:s') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Het daadwerkelijke bericht -->
|
||||||
|
<div class="text-xs sm:text-sm text-slate-800 leading-relaxed whitespace-pre-wrap font-normal p-4 rounded-xl bg-slate-50 border border-slate-200/60">
|
||||||
|
{{ $communication->message }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($communication->ip_address)
|
||||||
|
<div class="text-[10px] text-slate-400 flex items-center space-x-3 pt-1">
|
||||||
|
<span>IP-adres: {{ $communication->ip_address }}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>Afschrift verstuurd naar: {{ $communication->email }}</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reactie Geschiedenis (Replies) -->
|
||||||
|
@if($communication->replies->isNotEmpty())
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider flex items-center space-x-2">
|
||||||
|
<i data-lucide="corner-down-right" class="w-3.5 h-3.5 text-slate-400"></i>
|
||||||
|
<span>Eerdere Reacties ({{ $communication->replies->count() }})</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
@foreach($communication->replies as $reply)
|
||||||
|
<div class="bg-white rounded-2xl border border-emerald-200/80 shadow-xs p-5 space-y-3">
|
||||||
|
<div class="flex items-center justify-between pb-2 border-b border-slate-100">
|
||||||
|
<div class="flex items-center space-x-2.5">
|
||||||
|
<div class="w-7 h-7 rounded-lg bg-emerald-600 text-white flex items-center justify-center font-bold text-[10px]">
|
||||||
|
{{ $reply->user ? strtoupper(substr($reply->user->name, 0, 1)) : 'J3D' }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold text-slate-900 text-xs">{{ $reply->user?->name ?? 'Jouwdriedee.nl Beheer' }}</span>
|
||||||
|
<span class="text-[10px] text-emerald-700 font-medium ml-1.5 px-1.5 py-0.2 rounded bg-emerald-50">Verzonden via info@jouwdriedee.nl</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-[11px] text-slate-400 font-mono">
|
||||||
|
{{ $reply->created_at->format('d-m-Y H:i') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xs sm:text-sm text-slate-800 leading-relaxed whitespace-pre-wrap">
|
||||||
|
{{ $reply->message }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<!-- Direct Antwoord Formulier -->
|
||||||
|
<div class="bg-white rounded-2xl border border-slate-200/80 shadow-xs p-6 space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider flex items-center space-x-2">
|
||||||
|
<i data-lucide="reply" class="w-3.5 h-3.5 text-slate-500"></i>
|
||||||
|
<span>Beantwoord deze vraag direct</span>
|
||||||
|
</h3>
|
||||||
|
<p class="text-xs text-slate-500 mt-0.5">
|
||||||
|
Jouw antwoord wordt als e-mail verzonden vanuit <code class="text-slate-800 font-mono bg-slate-100 px-1 py-0.2 rounded">info@jouwdriedee.nl</code> naar <strong class="text-slate-800">{{ $communication->email }}</strong>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="{{ route('admin.communications.reply', $communication) }}" method="POST" class="space-y-3">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<textarea
|
||||||
|
name="message"
|
||||||
|
rows="5"
|
||||||
|
required
|
||||||
|
placeholder="Typ jouw reactie aan {{ $communication->name }}..."
|
||||||
|
class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3.5 text-xs sm:text-sm text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900 leading-relaxed"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between pt-1">
|
||||||
|
<div class="text-[11px] text-slate-400 flex items-center space-x-1.5">
|
||||||
|
<i data-lucide="mail-check" class="w-3.5 h-3.5 text-emerald-600"></i>
|
||||||
|
<span>Status wordt na verzending automatisch 'Beantwoord'</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="px-5 py-2.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs transition flex items-center space-x-2 shadow-xs cursor-pointer">
|
||||||
|
<i data-lucide="send" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>Verstuur Reactie naar Klant</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rechterkolom: Klantprofiel Info & Status Paneel -->
|
||||||
|
<div class="lg:col-span-4 space-y-5">
|
||||||
|
|
||||||
|
<!-- Status & Notities Box -->
|
||||||
|
<div class="bg-white p-5 rounded-2xl border border-slate-200/80 shadow-xs space-y-4">
|
||||||
|
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider">Status & Interne Notities</h3>
|
||||||
|
|
||||||
|
<form action="{{ route('admin.communications.status', $communication) }}" method="POST" class="space-y-3">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-xs font-medium text-slate-700">Dossier Status</label>
|
||||||
|
<select name="status" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-xs font-semibold text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900">
|
||||||
|
<option value="new" {{ $communication->status === 'new' ? 'selected' : '' }}>Nieuw</option>
|
||||||
|
<option value="in_progress" {{ $communication->status === 'in_progress' ? 'selected' : '' }}>In behandeling</option>
|
||||||
|
<option value="resolved" {{ $communication->status === 'resolved' ? 'selected' : '' }}>Beantwoord / Opgelost</option>
|
||||||
|
<option value="closed" {{ $communication->status === 'closed' ? 'selected' : '' }}>Gesloten</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-xs font-medium text-slate-700">Interne beheerder notitie</label>
|
||||||
|
<textarea name="admin_notes" rows="3" placeholder="Alleen zichtbaar voor beheerders..." class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900">{{ old('admin_notes', $communication->admin_notes) }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="w-full py-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-800 text-xs font-medium transition cursor-pointer">
|
||||||
|
Status & Notitie Opslaan
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Klantprofiel Kaart -->
|
||||||
|
<div class="bg-white p-5 rounded-2xl border border-slate-200/80 shadow-xs space-y-3.5">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider">Klantgegevens</h3>
|
||||||
|
<a href="{{ route('admin.customers.show', $communication->user_id ?? $communication->email) }}" class="text-[11px] font-semibold text-slate-900 underline hover:text-slate-700">
|
||||||
|
Volledig Profiel →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xs space-y-2 text-slate-600">
|
||||||
|
<div>
|
||||||
|
<span class="text-slate-400 block text-[10px]">Naam</span>
|
||||||
|
<a href="{{ route('admin.customers.show', $communication->user_id ?? $communication->email) }}" class="font-semibold text-slate-900 hover:underline">
|
||||||
|
{{ $communication->name }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="text-slate-400 block text-[10px]">E-mailadres</span>
|
||||||
|
<a href="mailto:{{ $communication->email }}" class="text-slate-900 hover:underline font-mono">
|
||||||
|
{{ $communication->email }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($communication->phone)
|
||||||
|
<div>
|
||||||
|
<span class="text-slate-400 block text-[10px]">Telefoonnummer</span>
|
||||||
|
<a href="tel:{{ $communication->phone }}" class="text-slate-900 hover:underline font-mono">
|
||||||
|
{{ $communication->phone }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="text-slate-400 block text-[10px]">Account status</span>
|
||||||
|
@if($communication->user)
|
||||||
|
<span class="inline-flex items-center space-x-1 text-emerald-700 font-semibold">
|
||||||
|
<i data-lucide="check" class="w-3 h-3"></i>
|
||||||
|
<span>Geregistreerd account (#{{ $communication->user->id }})</span>
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<span class="text-slate-500">Gastbezoeker (geen account)</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-2 border-t border-slate-100">
|
||||||
|
<a href="{{ route('admin.customers.show', $communication->user_id ?? $communication->email) }}" class="w-full py-2 px-3 rounded-xl bg-slate-50 hover:bg-slate-100 text-slate-800 text-xs font-medium transition flex items-center justify-center space-x-1.5">
|
||||||
|
<i data-lucide="user" class="w-3.5 h-3.5 text-slate-500"></i>
|
||||||
|
<span>Bekijk alle communicatie van deze klant</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gekoppelde Bestelling (indien van toepassing) -->
|
||||||
|
@if($communication->order)
|
||||||
|
<div class="bg-white p-5 rounded-2xl border border-slate-200/80 shadow-xs space-y-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider">Betreft Bestelling</h3>
|
||||||
|
<a href="{{ route('admin.orders.show', $communication->order) }}" class="text-[11px] font-semibold text-indigo-600 underline">
|
||||||
|
Order details →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xs space-y-1.5 text-slate-600">
|
||||||
|
<div>
|
||||||
|
<span class="text-slate-400 block text-[10px]">Ordernummer</span>
|
||||||
|
<span class="font-mono font-bold text-slate-900">#{{ $communication->order->order_number }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-slate-400 block text-[10px]">Orderdatum</span>
|
||||||
|
<span>{{ $communication->order->created_at->format('d-m-Y H:i') }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-slate-400 block text-[10px]">Bedrag & Status</span>
|
||||||
|
<span class="font-bold text-slate-900">{{ $communication->order->formatted_total }}</span>
|
||||||
|
<span class="ml-1 text-[10px] px-1.5 py-0.2 rounded bg-slate-100 font-semibold">{{ $communication->order->status }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<!-- Header Section -->
|
<!-- Header Section -->
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-6 border-b border-slate-200/60 gap-4">
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-6 border-b border-slate-200/60 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<span class="text-[11px] font-semibold uppercase tracking-wider text-slate-400 block mb-1">Content Management</span>
|
<span class="text-[11px] font-semibold uppercase tracking-wider text-slate-400 block mb-1">Shop Content & Stijl CMS</span>
|
||||||
<h1 class="font-heading text-2xl sm:text-3xl font-bold text-slate-900 tracking-tight">Layout & Teksten</h1>
|
<h1 class="font-heading text-2xl sm:text-3xl font-bold text-slate-900 tracking-tight">Layout & Teksten</h1>
|
||||||
<p class="text-xs sm:text-sm text-slate-500 mt-1">
|
<p class="text-xs sm:text-sm text-slate-500 mt-1">
|
||||||
Pas teksten, titels, USP's, kostprijstarieven en instellingen van de webshop aan.
|
Pas teksten, titels, USP's, kostprijstarieven en instellingen van de webshop aan.
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', 'Klanten & Profielen | Jouwdriedee Beheer')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="space-y-6">
|
||||||
|
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-6 border-b border-slate-200/80 gap-4">
|
||||||
|
<div>
|
||||||
|
<div class="inline-flex items-center space-x-1.5 text-xs text-slate-500 font-medium mb-1">
|
||||||
|
<i data-lucide="users" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>Relatiebeheer & Accounts</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-2xl font-bold tracking-tight text-slate-900">Klanten & Profielen</h1>
|
||||||
|
<p class="text-xs text-slate-500 mt-1">
|
||||||
|
Overzicht van geregistreerde klanten ({{ $totalRegistered }}) en gastkopers ({{ $totalGuests }}).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<a href="{{ route('admin.communications.index') }}" class="px-3.5 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-white text-xs font-medium transition flex items-center space-x-1.5 shadow-xs">
|
||||||
|
<i data-lucide="message-square" class="w-3.5 h-3.5 text-slate-400"></i>
|
||||||
|
<span>Communicatie Inbox</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Bar -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<form action="{{ route('admin.customers.index') }}" method="GET" class="flex items-center space-x-2 w-full sm:w-80">
|
||||||
|
<div class="relative w-full">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="q"
|
||||||
|
value="{{ $search }}"
|
||||||
|
placeholder="Zoek klant op naam of e-mail..."
|
||||||
|
class="w-full pl-8 pr-3 py-2 bg-white border border-slate-200/80 rounded-xl text-xs text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-slate-900 focus:border-slate-900"
|
||||||
|
>
|
||||||
|
<i data-lucide="search" class="w-3.5 h-3.5 text-slate-400 absolute left-2.5 top-1/2 -translate-y-1/2"></i>
|
||||||
|
</div>
|
||||||
|
@if($search)
|
||||||
|
<a href="{{ route('admin.customers.index') }}" class="px-2.5 py-2 text-xs text-slate-500 hover:text-slate-800 transition">
|
||||||
|
Wissen
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Customers Table Card -->
|
||||||
|
<div class="rounded-2xl bg-white border border-slate-200/80 overflow-hidden shadow-2xs">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left text-xs text-slate-700">
|
||||||
|
<thead class="bg-slate-50/80 text-[11px] uppercase tracking-wider text-slate-500 font-semibold border-b border-slate-200/70">
|
||||||
|
<tr>
|
||||||
|
<th class="py-3 px-4">Klant</th>
|
||||||
|
<th class="py-3 px-4">Type</th>
|
||||||
|
<th class="py-3 px-4">Bestellingen</th>
|
||||||
|
<th class="py-3 px-4">Communicatie / Vragen</th>
|
||||||
|
<th class="py-3 px-4">Actief sinds</th>
|
||||||
|
<th class="py-3 px-4 text-right">Profiel</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100 font-medium">
|
||||||
|
@forelse($customers as $customer)
|
||||||
|
<tr class="hover:bg-slate-50/60 transition">
|
||||||
|
|
||||||
|
<!-- Naam & E-mail -->
|
||||||
|
<td class="py-3.5 px-4">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<div class="w-8 h-8 rounded-xl bg-slate-900 text-white flex items-center justify-center font-bold text-xs shrink-0">
|
||||||
|
{{ strtoupper(substr($customer->name, 0, 1)) }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="{{ route('admin.customers.show', $customer->id ?? $customer->email) }}" class="font-bold text-slate-900 hover:underline">
|
||||||
|
{{ $customer->name }}
|
||||||
|
</a>
|
||||||
|
<div class="text-[11px] text-slate-400 font-normal">
|
||||||
|
{{ $customer->email }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Type -->
|
||||||
|
<td class="py-3.5 px-4">
|
||||||
|
@if($customer->is_registered)
|
||||||
|
<span class="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-emerald-50 text-emerald-700 border border-emerald-200/60">
|
||||||
|
Geregistreerd account
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<span class="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-slate-100 text-slate-600 border border-slate-200/60">
|
||||||
|
Gastkoper
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Bestellingen -->
|
||||||
|
<td class="py-3.5 px-4">
|
||||||
|
<span class="px-2 py-0.5 rounded-md bg-slate-100 text-slate-700 text-[11px] font-medium">
|
||||||
|
{{ $customer->orders_count }} order(s)
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Communicatie -->
|
||||||
|
<td class="py-3.5 px-4">
|
||||||
|
@if($customer->communications_count > 0)
|
||||||
|
<span class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-md bg-indigo-50 text-indigo-700 text-[11px] font-semibold border border-indigo-200/60">
|
||||||
|
<i data-lucide="message-square" class="w-3 h-3"></i>
|
||||||
|
<span>{{ $customer->communications_count }} bericht(en)</span>
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<span class="text-slate-400 text-[11px]">0 berichten</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Actief sinds -->
|
||||||
|
<td class="py-3.5 px-4 text-slate-500 whitespace-nowrap">
|
||||||
|
{{ $customer->created_at instanceof \Carbon\Carbon ? $customer->created_at->format('d-m-Y') : date('d-m-Y', strtotime((string)$customer->created_at)) }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Actie -->
|
||||||
|
<td class="py-3.5 px-4 text-right whitespace-nowrap">
|
||||||
|
<a href="{{ route('admin.customers.show', $customer->id ?? $customer->email) }}" class="inline-flex items-center space-x-1 px-2.5 py-1 rounded-lg bg-slate-100 hover:bg-slate-900 text-slate-700 hover:text-white text-xs font-medium transition">
|
||||||
|
<span>Bekijk profiel</span>
|
||||||
|
<i data-lucide="chevron-right" class="w-3 h-3"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="py-12 text-center text-slate-400">
|
||||||
|
Geen klanten gevonden die voldoen aan de zoekopdracht.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($customers->hasPages())
|
||||||
|
<div class="p-4 border-t border-slate-100">
|
||||||
|
{{ $customers->links() }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', 'Klantprofiel: ' . $customerName . ' | Jouwdriedee Beheer')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="space-y-6 max-w-6xl mx-auto" x-data="{ tab: 'communications' }">
|
||||||
|
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-6 border-b border-slate-200/80 gap-4">
|
||||||
|
<div>
|
||||||
|
<a href="{{ route('admin.customers.index') }}" class="text-xs text-slate-500 hover:text-slate-900 font-medium inline-flex items-center space-x-1 mb-1 transition">
|
||||||
|
<i data-lucide="arrow-left" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>Terug naar Klantenoverzicht</span>
|
||||||
|
</a>
|
||||||
|
<div class="flex items-center space-x-2 mt-0.5">
|
||||||
|
<h1 class="text-2xl font-bold tracking-tight text-slate-900">{{ $customerName }}</h1>
|
||||||
|
@if($user)
|
||||||
|
<span class="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-emerald-50 text-emerald-700 border border-emerald-200/60">
|
||||||
|
Geregistreerd account (#{{ $user->id }})
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<span class="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-slate-100 text-slate-600 border border-slate-200/60">
|
||||||
|
Gastkoper
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-slate-500 mt-0.5">
|
||||||
|
{{ $email }} @if($customerPhone) • {{ $customerPhone }} @endif
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<a href="{{ route('admin.communications.index') }}" class="px-3.5 py-2 rounded-xl bg-white hover:bg-slate-50 text-slate-700 text-xs font-medium transition flex items-center space-x-1.5 border border-slate-200/80 shadow-2xs">
|
||||||
|
<i data-lucide="inbox" class="w-3.5 h-3.5 text-slate-400"></i>
|
||||||
|
<span>Alle Communicatie</span>
|
||||||
|
</a>
|
||||||
|
<a href="mailto:{{ $email }}" class="px-3.5 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-white text-xs font-medium transition flex items-center space-x-1.5 shadow-xs">
|
||||||
|
<i data-lucide="mail" class="w-3.5 h-3.5 text-slate-400"></i>
|
||||||
|
<span>E-mail Sturen</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Stats Cards -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
|
||||||
|
<div class="p-4 rounded-2xl bg-white border border-slate-200/80 shadow-2xs flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<span class="text-[11px] font-semibold text-slate-400 uppercase tracking-wider block">Communicatie Dossier</span>
|
||||||
|
<span class="text-xl font-bold text-slate-900 mt-0.5 block">{{ $communications->count() }} vragen / berichten</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-10 h-10 rounded-xl bg-indigo-50 text-indigo-600 flex items-center justify-center">
|
||||||
|
<i data-lucide="message-square" class="w-5 h-5"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 rounded-2xl bg-white border border-slate-200/80 shadow-2xs flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<span class="text-[11px] font-semibold text-slate-400 uppercase tracking-wider block">Bestellingen</span>
|
||||||
|
<span class="text-xl font-bold text-slate-900 mt-0.5 block">{{ $orders->count() }} orders</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-10 h-10 rounded-xl bg-sky-50 text-sky-600 flex items-center justify-center">
|
||||||
|
<i data-lucide="shopping-cart" class="w-5 h-5"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 rounded-2xl bg-white border border-slate-200/80 shadow-2xs flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<span class="text-[11px] font-semibold text-slate-400 uppercase tracking-wider block">Totale Omzet</span>
|
||||||
|
<span class="text-xl font-bold text-slate-900 mt-0.5 block">€ {{ number_format($totalSpent, 2, ',', '.') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-10 h-10 rounded-xl bg-emerald-50 text-emerald-600 flex items-center justify-center">
|
||||||
|
<i data-lucide="receipt" class="w-5 h-5"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Segmented Navigation Tabs -->
|
||||||
|
<div class="flex items-center space-x-1.5 p-1 bg-slate-100/80 rounded-xl max-w-max text-xs font-medium">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="tab = 'communications'"
|
||||||
|
:class="tab === 'communications' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900'"
|
||||||
|
class="px-3.5 py-1.5 rounded-lg transition flex items-center space-x-1.5 cursor-pointer"
|
||||||
|
>
|
||||||
|
<i data-lucide="message-square" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>Communicatie & Vragen</span>
|
||||||
|
<span class="px-1.5 py-0.2 rounded-md text-[10px] bg-slate-200/60 text-slate-700">
|
||||||
|
{{ $communications->count() }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="tab = 'orders'"
|
||||||
|
:class="tab === 'orders' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900'"
|
||||||
|
class="px-3.5 py-1.5 rounded-lg transition flex items-center space-x-1.5 cursor-pointer"
|
||||||
|
>
|
||||||
|
<i data-lucide="shopping-bag" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>Bestelgeschiedenis</span>
|
||||||
|
<span class="px-1.5 py-0.2 rounded-md text-[10px] bg-slate-200/60 text-slate-700">
|
||||||
|
{{ $orders->count() }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB 1: COMMUNICATIE & VRAGEN BINNEN DIT KLANTPROFIEL -->
|
||||||
|
<div x-show="tab === 'communications'" class="space-y-4">
|
||||||
|
@if($communications->isEmpty())
|
||||||
|
<div class="p-12 text-center rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-2">
|
||||||
|
<div class="w-10 h-10 rounded-xl bg-slate-100 text-slate-400 flex items-center justify-center mx-auto mb-1">
|
||||||
|
<i data-lucide="message-square-off" class="w-5 h-5"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-sm font-semibold text-slate-800">Geen communicatie gevonden</h3>
|
||||||
|
<p class="text-xs text-slate-500 max-w-sm mx-auto">
|
||||||
|
Deze klant heeft nog geen vragen gesteld via het contactformulier.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="space-y-3">
|
||||||
|
@foreach($communications as $comm)
|
||||||
|
<div class="p-5 rounded-2xl bg-white border border-slate-200/80 shadow-2xs space-y-3 hover:border-slate-300 transition">
|
||||||
|
|
||||||
|
<!-- Header regel -->
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-2.5 border-b border-slate-100 gap-2">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span class="font-mono text-xs text-slate-400">#{{ $comm->id }}</span>
|
||||||
|
<h3 class="font-bold text-slate-900 text-sm">{{ $comm->subject }}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-semibold
|
||||||
|
@if($comm->status === 'new') bg-sky-50 text-sky-700 border border-sky-200/80
|
||||||
|
@elseif($comm->status === 'in_progress') bg-amber-50 text-amber-700 border border-amber-200/80
|
||||||
|
@elseif($comm->status === 'resolved') bg-emerald-50 text-emerald-700 border border-emerald-200/80
|
||||||
|
@else bg-slate-100 text-slate-600 border border-slate-200/60
|
||||||
|
@endif">
|
||||||
|
{{ $comm->status_label }}
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-slate-400 font-mono">
|
||||||
|
{{ $comm->created_at->format('d-m-Y H:i') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Vraag tekst -->
|
||||||
|
<div class="text-xs text-slate-700 leading-relaxed bg-slate-50 p-3 rounded-xl border border-slate-100">
|
||||||
|
{{ $comm->message }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer info & link naar communicatiedossier -->
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between pt-1 gap-2 text-xs">
|
||||||
|
<div class="flex items-center space-x-3 text-slate-500">
|
||||||
|
@if($comm->order)
|
||||||
|
<span>Betreft: <a href="{{ route('admin.orders.show', $comm->order) }}" class="font-mono text-indigo-600 hover:underline">#{{ $comm->order->order_number }}</a></span>
|
||||||
|
<span>•</span>
|
||||||
|
@endif
|
||||||
|
<span>{{ $comm->replies->count() }} antwoord(en) verzonden</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{{ route('admin.communications.show', $comm) }}" class="inline-flex items-center space-x-1.5 px-3 py-1.5 rounded-lg bg-slate-900 hover:bg-slate-800 text-white text-xs font-medium transition shadow-xs self-start sm:self-auto">
|
||||||
|
<span>Open Bericht & Beantwoord</span>
|
||||||
|
<i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB 2: BESTELGESCHIEDENIS BINNEN DIT KLANTPROFIEL -->
|
||||||
|
<div x-show="tab === 'orders'" class="space-y-4">
|
||||||
|
@if($orders->isEmpty())
|
||||||
|
<div class="p-12 text-center rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-2">
|
||||||
|
<div class="w-10 h-10 rounded-xl bg-slate-100 text-slate-400 flex items-center justify-center mx-auto mb-1">
|
||||||
|
<i data-lucide="shopping-cart" class="w-5 h-5"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-sm font-semibold text-slate-800">Nog geen bestellingen</h3>
|
||||||
|
<p class="text-xs text-slate-500 max-w-sm mx-auto">
|
||||||
|
Deze klant heeft nog geen geplaatste orders in het systeem.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="rounded-2xl bg-white border border-slate-200/80 overflow-hidden shadow-2xs">
|
||||||
|
<table class="w-full text-left text-xs text-slate-700">
|
||||||
|
<thead class="bg-slate-50/80 text-[11px] uppercase tracking-wider text-slate-500 font-semibold border-b border-slate-200/70">
|
||||||
|
<tr>
|
||||||
|
<th class="py-3 px-4">Order #</th>
|
||||||
|
<th class="py-3 px-4">Datum</th>
|
||||||
|
<th class="py-3 px-4">Items</th>
|
||||||
|
<th class="py-3 px-4">Totaal</th>
|
||||||
|
<th class="py-3 px-4">Status</th>
|
||||||
|
<th class="py-3 px-4 text-right">Details</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100 font-medium">
|
||||||
|
@foreach($orders as $order)
|
||||||
|
<tr class="hover:bg-slate-50/60 transition">
|
||||||
|
<td class="py-3.5 px-4 font-mono font-bold text-slate-900">
|
||||||
|
{{ $order->order_number }}
|
||||||
|
</td>
|
||||||
|
<td class="py-3.5 px-4 text-slate-500">
|
||||||
|
{{ $order->created_at->format('d-m-Y H:i') }}
|
||||||
|
</td>
|
||||||
|
<td class="py-3.5 px-4">
|
||||||
|
{{ $order->items->count() }} prints
|
||||||
|
</td>
|
||||||
|
<td class="py-3.5 px-4 font-bold text-slate-900">
|
||||||
|
{{ $order->formatted_total }}
|
||||||
|
</td>
|
||||||
|
<td class="py-3.5 px-4">
|
||||||
|
<span class="px-2 py-0.5 rounded-full text-[10px] font-semibold
|
||||||
|
@if($order->status === 'completed' || $order->status === 'shipped') bg-emerald-50 text-emerald-700 border border-emerald-200/60
|
||||||
|
@elseif($order->status === 'printed' || $order->status === 'processing') bg-sky-50 text-sky-700 border border-sky-200/60
|
||||||
|
@elseif($order->status === 'cancelled') bg-rose-50 text-rose-700 border border-rose-200/60
|
||||||
|
@else bg-amber-50 text-amber-700 border border-amber-200/60
|
||||||
|
@endif">
|
||||||
|
{{ $order->status_label }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-3.5 px-4 text-right">
|
||||||
|
<a href="{{ route('admin.orders.show', $order) }}" class="inline-flex items-center space-x-1 px-2.5 py-1 rounded-lg bg-slate-100 hover:bg-slate-900 text-slate-700 hover:text-white text-xs font-medium transition">
|
||||||
|
<span>Order bekijken</span>
|
||||||
|
<i data-lucide="chevron-right" class="w-3 h-3"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -88,12 +88,19 @@
|
|||||||
<!-- Klant & Verzendinfo -->
|
<!-- Klant & Verzendinfo -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="p-5 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-3">
|
<div class="p-5 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
<h3 class="font-bold text-xs text-slate-900 uppercase tracking-wider">Klantgegevens</h3>
|
<h3 class="font-bold text-xs text-slate-900 uppercase tracking-wider">Klantgegevens</h3>
|
||||||
|
<a href="{{ route('admin.customers.show', $order->user_id ?? $order->customer_email) }}" class="text-[11px] font-semibold text-slate-900 underline hover:text-slate-700">
|
||||||
|
Klantprofiel →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="text-xs space-y-1.5 text-slate-600">
|
<div class="text-xs space-y-1.5 text-slate-600">
|
||||||
<div>
|
<div>
|
||||||
<span class="text-slate-400 block text-[10px]">Naam</span>
|
<span class="text-slate-400 block text-[10px]">Naam</span>
|
||||||
<span class="font-semibold text-slate-900">{{ $order->customer_name }}</span>
|
<a href="{{ route('admin.customers.show', $order->user_id ?? $order->customer_email) }}" class="font-semibold text-slate-900 hover:underline">
|
||||||
|
{{ $order->customer_name }}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="text-slate-400 block text-[10px]">E-mailadres</span>
|
<span class="text-slate-400 block text-[10px]">E-mailadres</span>
|
||||||
|
|||||||
@@ -338,7 +338,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<i data-lucide="image" class="w-4 h-4 text-slate-500"></i>
|
<i data-lucide="image" class="w-4 h-4 text-slate-500"></i>
|
||||||
<h3 class="text-xs font-semibold text-slate-900 uppercase tracking-wider">Afbeeldingen</h3>
|
<h3 class="text-xs font-semibold text-slate-900 uppercase tracking-wider">Productafbeeldingen & Fotogalerij</h3>
|
||||||
<span class="px-2 py-0.5 rounded-full bg-slate-100 text-slate-600 font-mono text-[11px]" x-text="(imageUrl ? 1 : 0) + galleryImages.length + ' foto(\'s)'"></span>
|
<span class="px-2 py-0.5 rounded-full bg-slate-100 text-slate-600 font-mono text-[11px]" x-text="(imageUrl ? 1 : 0) + galleryImages.length + ' foto(\'s)'"></span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-slate-500 mt-0.5">
|
<p class="text-xs text-slate-500 mt-0.5">
|
||||||
|
|||||||
@@ -347,7 +347,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<i data-lucide="image" class="w-4 h-4 text-slate-500"></i>
|
<i data-lucide="image" class="w-4 h-4 text-slate-500"></i>
|
||||||
<h3 class="text-xs font-semibold text-slate-900 uppercase tracking-wider">Afbeeldingen</h3>
|
<h3 class="text-xs font-semibold text-slate-900 uppercase tracking-wider">Productafbeeldingen & Fotogalerij</h3>
|
||||||
<span class="px-2 py-0.5 rounded-full bg-slate-100 text-slate-600 font-mono text-[11px]" x-text="(imageUrl ? 1 : 0) + galleryImages.length + ' foto(\'s)'"></span>
|
<span class="px-2 py-0.5 rounded-full bg-slate-100 text-slate-600 font-mono text-[11px]" x-text="(imageUrl ? 1 : 0) + galleryImages.length + ' foto(\'s)'"></span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-slate-500 mt-0.5">
|
<p class="text-xs text-slate-500 mt-0.5">
|
||||||
@@ -394,7 +394,7 @@
|
|||||||
<!-- Cover Box -->
|
<!-- Cover Box -->
|
||||||
<div class="md:col-span-5 bg-slate-50/70 rounded-xl border border-slate-200/80 p-3 space-y-2 relative overflow-hidden">
|
<div class="md:col-span-5 bg-slate-50/70 rounded-xl border border-slate-200/80 p-3 space-y-2 relative overflow-hidden">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-[10px] font-semibold text-slate-500 uppercase tracking-wider">Hoofdafbeelding</span>
|
<span class="text-[10px] font-semibold text-slate-500 uppercase tracking-wider">Hoofdafbeelding (Cover)</span>
|
||||||
<template x-if="imageUrl">
|
<template x-if="imageUrl">
|
||||||
<button type="button" @click="removeCover" class="text-slate-400 hover:text-rose-600 text-[11px] transition">
|
<button type="button" @click="removeCover" class="text-slate-400 hover:text-rose-600 text-[11px] transition">
|
||||||
Verwijderen
|
Verwijderen
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Vraag stellen & Contact | Jouwdriedee.nl')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<!-- Header Banner -->
|
||||||
|
<div class="border-b border-slate-200/80 py-8 sm:py-12 bg-slate-50/50">
|
||||||
|
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center space-y-2.5">
|
||||||
|
<span class="px-3 py-1 rounded-full bg-white border border-slate-200 text-xs font-medium text-slate-700 inline-flex items-center space-x-1.5 shadow-2xs">
|
||||||
|
<i data-lucide="message-square" class="w-3.5 h-3.5 text-slate-500"></i>
|
||||||
|
<span>Klantenservice & Advies</span>
|
||||||
|
</span>
|
||||||
|
<h1 class="text-3xl sm:text-4xl font-bold tracking-tight text-slate-900">Stel jouw vraag aan de printstudio</h1>
|
||||||
|
<p class="text-xs sm:text-sm text-slate-500 max-w-xl mx-auto leading-relaxed">
|
||||||
|
Heb je een vraag over een 3D-model, een lopende bestelling of advies nodig over filament of kleuren? Stuur ons gerust een bericht. Je ontvangt direct een afschrift per e-mail.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-12">
|
||||||
|
|
||||||
|
<!-- Success Alert -->
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="mb-8 p-4 rounded-2xl bg-emerald-50 border border-emerald-200/60 text-emerald-800 text-xs flex items-start space-x-3 shadow-xs">
|
||||||
|
<i data-lucide="check-circle" class="w-5 h-5 text-emerald-600 shrink-0 mt-0.5"></i>
|
||||||
|
<div>
|
||||||
|
<strong class="font-semibold block text-sm mb-0.5">Bericht succesvol verzonden!</strong>
|
||||||
|
<span>{{ session('success') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
|
||||||
|
|
||||||
|
<!-- Contactformulier Card -->
|
||||||
|
<div class="lg:col-span-7 bg-white p-6 sm:p-8 rounded-2xl border border-slate-200/80 shadow-xs space-y-6">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-base font-bold text-slate-900">Stel een vraag</h2>
|
||||||
|
<p class="text-xs text-slate-500 mt-0.5">Vul onderstaand formulier in, dan reageren wij binnen 24 uur.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="{{ route('contact.store') }}" method="POST" class="space-y-4">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<!-- Naam & E-mail -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label for="name" class="block text-xs font-medium text-slate-700">Jouw naam *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
id="name"
|
||||||
|
required
|
||||||
|
value="{{ old('name', $user->name ?? '') }}"
|
||||||
|
placeholder="Jan Jansen"
|
||||||
|
class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900 @error('name') border-rose-400 @enderror"
|
||||||
|
>
|
||||||
|
@error('name')
|
||||||
|
<p class="text-[11px] text-rose-600 mt-1">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label for="email" class="block text-xs font-medium text-slate-700">E-mailadres *</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
id="email"
|
||||||
|
required
|
||||||
|
value="{{ old('email', $user->email ?? '') }}"
|
||||||
|
placeholder="jouw@email.nl"
|
||||||
|
class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900 @error('email') border-rose-400 @enderror"
|
||||||
|
>
|
||||||
|
@error('email')
|
||||||
|
<p class="text-[11px] text-rose-600 mt-1">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Telefoon & Bestelnummer -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label for="phone" class="block text-xs font-medium text-slate-700">Telefoonnummer (optioneel)</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
name="phone"
|
||||||
|
id="phone"
|
||||||
|
value="{{ old('phone') }}"
|
||||||
|
placeholder="06-12345678"
|
||||||
|
class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label for="order_number" class="block text-xs font-medium text-slate-700">Bestelnummer (optioneel)</label>
|
||||||
|
@if($userOrders->isNotEmpty())
|
||||||
|
<select
|
||||||
|
name="order_number"
|
||||||
|
id="order_number"
|
||||||
|
class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900"
|
||||||
|
>
|
||||||
|
<option value="">-- Geen specifieke bestelling --</option>
|
||||||
|
@foreach($userOrders as $order)
|
||||||
|
<option value="{{ $order->order_number }}" {{ old('order_number') === $order->order_number ? 'selected' : '' }}>
|
||||||
|
Order #{{ $order->order_number }} ({{ $order->created_at->format('d-m-Y') }})
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@else
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="order_number"
|
||||||
|
id="order_number"
|
||||||
|
value="{{ old('order_number') }}"
|
||||||
|
placeholder="bijv. ORD-2026-..."
|
||||||
|
class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900"
|
||||||
|
>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Onderwerp -->
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label for="subject" class="block text-xs font-medium text-slate-700">Onderwerp van jouw vraag *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="subject"
|
||||||
|
id="subject"
|
||||||
|
required
|
||||||
|
value="{{ old('subject') }}"
|
||||||
|
placeholder="bijv. Vraag over levertijd of materiaalkeuze..."
|
||||||
|
class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900 @error('subject') border-rose-400 @enderror"
|
||||||
|
>
|
||||||
|
@error('subject')
|
||||||
|
<p class="text-[11px] text-rose-600 mt-1">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bericht -->
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label for="message" class="block text-xs font-medium text-slate-700">Jouw bericht of vraag *</label>
|
||||||
|
<textarea
|
||||||
|
name="message"
|
||||||
|
id="message"
|
||||||
|
rows="5"
|
||||||
|
required
|
||||||
|
placeholder="Typ hier jouw vraag zo duidelijk mogelijk..."
|
||||||
|
class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3.5 text-xs text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900 leading-relaxed @error('message') border-rose-400 @enderror"
|
||||||
|
>{{ old('message') }}</textarea>
|
||||||
|
@error('message')
|
||||||
|
<p class="text-[11px] text-rose-600 mt-1">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Note over afschrift -->
|
||||||
|
<div class="p-3 rounded-xl bg-slate-50 border border-slate-200/80 text-[11px] text-slate-500 flex items-center space-x-2">
|
||||||
|
<i data-lucide="shield-check" class="w-4 h-4 text-slate-400 shrink-0"></i>
|
||||||
|
<span>Je ontvangt na verzending direct een <strong>bevestigingsafschrift</strong> via <code class="text-slate-700 font-mono">noreply@jouwdriedee.nl</code>.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<div class="pt-1">
|
||||||
|
<button type="submit" class="w-full py-3 px-5 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs transition flex items-center justify-center space-x-2 shadow-xs cursor-pointer">
|
||||||
|
<i data-lucide="send" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>Vraag Versturen</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rechterkolom: Info & FAQ links -->
|
||||||
|
<div class="lg:col-span-5 space-y-5">
|
||||||
|
|
||||||
|
<!-- Quick Studio Info -->
|
||||||
|
<div class="bg-white p-6 rounded-2xl border border-slate-200/80 shadow-xs space-y-4">
|
||||||
|
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider">Directe Contactgegevens</h3>
|
||||||
|
|
||||||
|
<div class="space-y-3 text-xs text-slate-600">
|
||||||
|
<div class="flex items-start space-x-3">
|
||||||
|
<div class="w-8 h-8 rounded-lg bg-slate-100 flex items-center justify-center text-slate-500 shrink-0 mt-0.5">
|
||||||
|
<i data-lucide="mail" class="w-4 h-4"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-[10px] text-slate-400 uppercase font-medium block">E-mail Klantenservice</span>
|
||||||
|
<a href="mailto:info@jouwdriedee.nl" class="font-semibold text-slate-900 hover:underline">info@jouwdriedee.nl</a>
|
||||||
|
<span class="block text-[11px] text-slate-400">Reactie doorgaans binnen 24 uur</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start space-x-3">
|
||||||
|
<div class="w-8 h-8 rounded-lg bg-slate-100 flex items-center justify-center text-slate-500 shrink-0 mt-0.5">
|
||||||
|
<i data-lucide="clock" class="w-4 h-4"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-[10px] text-slate-400 uppercase font-medium block">Openingstijden Studio</span>
|
||||||
|
<span class="font-semibold text-slate-900">Maandag t/m Vrijdag: 09:00 - 18:00</span>
|
||||||
|
<span class="block text-[11px] text-slate-400">Printers draaien 24/7 door</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start space-x-3">
|
||||||
|
<div class="w-8 h-8 rounded-lg bg-slate-100 flex items-center justify-center text-slate-500 shrink-0 mt-0.5">
|
||||||
|
<i data-lucide="truck" class="w-4 h-4"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-[10px] text-slate-400 uppercase font-medium block">Verzending & Levering</span>
|
||||||
|
<span class="font-semibold text-slate-900">PostNL Track & Trace</span>
|
||||||
|
<span class="block text-[11px] text-slate-400">Veilig en stootvast verpakt</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Snelle Links -->
|
||||||
|
<div class="bg-white p-6 rounded-2xl border border-slate-200/80 shadow-xs space-y-3">
|
||||||
|
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider">Veelgestelde Hulp</h3>
|
||||||
|
|
||||||
|
<div class="space-y-2 text-xs">
|
||||||
|
<a href="{{ route('custom.request') }}" class="p-2.5 rounded-xl bg-slate-50 hover:bg-slate-100 transition flex items-center justify-between group">
|
||||||
|
<div class="flex items-center space-x-2.5">
|
||||||
|
<i data-lucide="layers" class="w-4 h-4 text-slate-500 group-hover:text-slate-900"></i>
|
||||||
|
<span class="font-medium text-slate-700 group-hover:text-slate-900">Eigen STL / 3MF bestand uploaden</span>
|
||||||
|
</div>
|
||||||
|
<i data-lucide="chevron-right" class="w-3.5 h-3.5 text-slate-400"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ route('catalog') }}" class="p-2.5 rounded-xl bg-slate-50 hover:bg-slate-100 transition flex items-center justify-between group">
|
||||||
|
<div class="flex items-center space-x-2.5">
|
||||||
|
<i data-lucide="box" class="w-4 h-4 text-slate-500 group-hover:text-slate-900"></i>
|
||||||
|
<span class="font-medium text-slate-700 group-hover:text-slate-900">Assortiment & Collecties bekijken</span>
|
||||||
|
</div>
|
||||||
|
<i data-lucide="chevron-right" class="w-3.5 h-3.5 text-slate-400"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
@auth
|
||||||
|
<a href="{{ route('account.index') }}" class="p-2.5 rounded-xl bg-slate-50 hover:bg-slate-100 transition flex items-center justify-between group">
|
||||||
|
<div class="flex items-center space-x-2.5">
|
||||||
|
<i data-lucide="user" class="w-4 h-4 text-slate-500 group-hover:text-slate-900"></i>
|
||||||
|
<span class="font-medium text-slate-700 group-hover:text-slate-900">Jouw bestelstatus & account</span>
|
||||||
|
</div>
|
||||||
|
<i data-lucide="chevron-right" class="w-3.5 h-3.5 text-slate-400"></i>
|
||||||
|
</a>
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
@extends('emails.layouts.master')
|
||||||
|
|
||||||
|
@section('title', 'Afschrift van jouw vraag aan Jouwdriedee.nl')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td style="padding-bottom: 20px;">
|
||||||
|
<span style="display: inline-block; background-color: #ECFDF5; color: #059669; font-size: 11px; font-weight: 700; padding: 4px 10px; border-radius: 9999px; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||||
|
Afschrift Klantvraag Ontvangen
|
||||||
|
</span>
|
||||||
|
<h1 style="margin: 12px 0 6px 0; font-size: 22px; font-weight: 800; color: #0F172A; line-height: 1.3;">
|
||||||
|
Bedankt voor je vraag, {{ $communication->name }}!
|
||||||
|
</h1>
|
||||||
|
<p style="margin: 0; font-size: 14px; color: #64748B; line-height: 1.5;">
|
||||||
|
We hebben jouw bericht in goede orde ontvangen. Onze 3D-printspecialisten bekijken jouw vraag zorgvuldig en nemen doorgaans <strong>binnen 24 uur</strong> (op werkdagen) contact met je op.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Samenvatting Afschrift Card -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 16px; background-color: #F8FAFC; border-radius: 12px; border: 1px solid #E2E8F0;">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 0; font-size: 13px; color: #64748B; width: 130px;"><strong>Onderwerp:</strong></td>
|
||||||
|
<td style="padding: 4px 0; font-size: 13px; color: #0F172A; font-weight: 600;">{{ $communication->subject }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 0; font-size: 13px; color: #64748B;"><strong>Referentienummer:</strong></td>
|
||||||
|
<td style="padding: 4px 0; font-size: 13px; color: #0F172A; font-family: monospace; font-weight: 700;">#{{ $communication->id }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 0; font-size: 13px; color: #64748B;"><strong>Datum & tijd:</strong></td>
|
||||||
|
<td style="padding: 4px 0; font-size: 13px; color: #0F172A;">{{ $communication->created_at->format('d-m-Y om H:i') }}</td>
|
||||||
|
</tr>
|
||||||
|
@if($communication->order)
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 0; font-size: 13px; color: #64748B;"><strong>Betreft bestelling:</strong></td>
|
||||||
|
<td style="padding: 4px 0; font-size: 13px; color: #0F172A; font-family: monospace;">#{{ $communication->order->order_number }}</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Jouw verstuurde bericht -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding-top: 20px;">
|
||||||
|
<h3 style="margin: 0 0 10px 0; font-size: 13px; font-weight: 700; color: #64748B; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||||
|
Kopie van jouw bericht:
|
||||||
|
</h3>
|
||||||
|
<div style="background-color: #FFFFFF; border: 1px solid #E2E8F0; border-radius: 8px; padding: 16px; font-size: 13px; line-height: 1.6; color: #334155; white-space: pre-wrap;">{{ $communication->message }}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Info note -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding-top: 24px;">
|
||||||
|
<p style="margin: 0; font-size: 12px; color: #94A3B8; line-height: 1.5; text-align: center;">
|
||||||
|
Dit is een automatisch verzonden afschrift vanuit <strong>noreply@jouwdriedee.nl</strong> ter bevestiging van je aanvraag.<br>
|
||||||
|
Heb je aanvullende informatie? Reageer dan gerust zodra je ons persoonlijke antwoord ontvangt.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
@extends('emails.layouts.master')
|
||||||
|
|
||||||
|
@section('title', 'Nieuwe klantvraag #' . $communication->id)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td style="padding-bottom: 20px;">
|
||||||
|
<span style="display: inline-block; background-color: #EEF2FF; color: #4F46E5; font-size: 11px; font-weight: 700; padding: 4px 10px; border-radius: 9999px; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||||
|
Nieuwe Klantvraag #{{ $communication->id }}
|
||||||
|
</span>
|
||||||
|
<h1 style="margin: 12px 0 6px 0; font-size: 22px; font-weight: 800; color: #0F172A; line-height: 1.3;">
|
||||||
|
{{ $communication->subject }}
|
||||||
|
</h1>
|
||||||
|
<p style="margin: 0; font-size: 13px; color: #64748B;">
|
||||||
|
Binnengekomen via het contactformulier op {{ $communication->created_at->format('d-m-Y om H:i') }}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Klantgegevens Card -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 16px; background-color: #F8FAFC; border-radius: 12px; border: 1px solid #E2E8F0; margin-bottom: 20px;">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 0; font-size: 13px; color: #64748B; width: 120px;"><strong>Klantnaam:</strong></td>
|
||||||
|
<td style="padding: 4px 0; font-size: 13px; color: #0F172A; font-weight: 600;">{{ $communication->name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 0; font-size: 13px; color: #64748B;"><strong>E-mailadres:</strong></td>
|
||||||
|
<td style="padding: 4px 0; font-size: 13px; color: #0F172A;"><a href="mailto:{{ $communication->email }}" style="color: #4F46E5; text-decoration: none;">{{ $communication->email }}</a></td>
|
||||||
|
</tr>
|
||||||
|
@if($communication->phone)
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 0; font-size: 13px; color: #64748B;"><strong>Telefoonnummer:</strong></td>
|
||||||
|
<td style="padding: 4px 0; font-size: 13px; color: #0F172A;">{{ $communication->phone }}</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
@if($communication->order)
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 0; font-size: 13px; color: #64748B;"><strong>Bestelnummer:</strong></td>
|
||||||
|
<td style="padding: 4px 0; font-size: 13px; color: #0F172A; font-family: monospace; font-weight: 700;">
|
||||||
|
#{{ $communication->order->order_number }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
@if($communication->user)
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 0; font-size: 13px; color: #64748B;"><strong>Account status:</strong></td>
|
||||||
|
<td style="padding: 4px 0; font-size: 13px; color: #059669; font-weight: 600;">Geregistreerde klant (ID: #{{ $communication->user->id }})</td>
|
||||||
|
</tr>
|
||||||
|
@else
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 0; font-size: 13px; color: #64748B;"><strong>Account status:</strong></td>
|
||||||
|
<td style="padding: 4px 0; font-size: 13px; color: #64748B;">Gastbezoeker</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Bericht van de klant -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding-top: 20px;">
|
||||||
|
<h3 style="margin: 0 0 10px 0; font-size: 14px; font-weight: 700; color: #0F172A; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||||
|
Bericht van de klant:
|
||||||
|
</h3>
|
||||||
|
<div style="background-color: #FFFFFF; border: 1px solid #CBD5E1; border-left: 4px solid #4F46E5; border-radius: 8px; padding: 18px; font-size: 14px; line-height: 1.6; color: #1E293B; white-space: pre-wrap;">{{ $communication->message }}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- CTA knop naar Admin -->
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding-top: 28px; padding-bottom: 10px;">
|
||||||
|
<a href="{{ route('admin.communications.show', $communication) }}" style="display: inline-block; background-color: #0F172A; color: #FFFFFF; text-decoration: none; padding: 12px 24px; border-radius: 10px; font-size: 13px; font-weight: 600;">
|
||||||
|
Bekijk & Beantwoord in Admin →
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
@extends('emails.layouts.master')
|
||||||
|
|
||||||
|
@section('title', 'Reactie op vraag #' . $communication->id)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td style="padding-bottom: 20px;">
|
||||||
|
<span style="display: inline-block; background-color: #EEF2FF; color: #4F46E5; font-size: 11px; font-weight: 700; padding: 4px 10px; border-radius: 9999px; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||||
|
Antwoord van Jouwdriedee.nl
|
||||||
|
</span>
|
||||||
|
<h1 style="margin: 12px 0 6px 0; font-size: 22px; font-weight: 800; color: #0F172A; line-height: 1.3;">
|
||||||
|
Beste {{ $communication->name }},
|
||||||
|
</h1>
|
||||||
|
<p style="margin: 0; font-size: 14px; color: #64748B;">
|
||||||
|
We hebben een reactie voor je klaarstaan op jouw vraag met betrekking tot <strong>{{ $communication->subject }}</strong>.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Reactie box -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 20px; background-color: #F8FAFC; border-radius: 12px; border: 1px solid #E2E8F0; border-left: 4px solid #059669;">
|
||||||
|
<h3 style="margin: 0 0 10px 0; font-size: 13px; font-weight: 700; color: #059669; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||||
|
Ons antwoord:
|
||||||
|
</h3>
|
||||||
|
<div style="font-size: 14px; line-height: 1.6; color: #0F172A; white-space: pre-wrap;">{{ $reply->message }}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Oorspronkelijk bericht -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding-top: 24px;">
|
||||||
|
<h4 style="margin: 0 0 8px 0; font-size: 12px; font-weight: 700; color: #94A3B8; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||||
|
Jouw eerdere vraag (#{{ $communication->id }}):
|
||||||
|
</h4>
|
||||||
|
<div style="background-color: #FFFFFF; border: 1px solid #E2E8F0; border-radius: 8px; padding: 14px; font-size: 12px; line-height: 1.5; color: #64748B; white-space: pre-wrap;">{{ $communication->message }}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding-top: 24px; text-align: center;">
|
||||||
|
<p style="margin: 0; font-size: 13px; color: #64748B;">
|
||||||
|
Heb je nog meer vragen? Je kunt direct antwoorden op deze e-mail of contact opnemen via <a href="mailto:info@jouwdriedee.nl" style="color: #4F46E5; text-decoration: none; font-weight: 600;">info@jouwdriedee.nl</a>.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
@endsection
|
||||||
@@ -73,6 +73,24 @@
|
|||||||
<span>Bestellingen</span>
|
<span>Bestellingen</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
@php
|
||||||
|
$unreadCommCount = \App\Models\Communication::where('status', 'new')->count();
|
||||||
|
@endphp
|
||||||
|
<a href="{{ route('admin.communications.index') }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ request()->routeIs('admin.communications.*') ? 'bg-slate-900 text-white font-semibold' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-100' }}">
|
||||||
|
<i data-lucide="message-square" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>Communicatie</span>
|
||||||
|
@if($unreadCommCount > 0)
|
||||||
|
<span class="ml-1 px-1.5 py-0.2 rounded-full text-[10px] font-bold {{ request()->routeIs('admin.communications.*') ? 'bg-white text-slate-900' : 'bg-sky-500 text-white animate-pulse' }}">
|
||||||
|
{{ $unreadCommCount }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ route('admin.customers.index') }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ request()->routeIs('admin.customers.*') ? 'bg-slate-900 text-white font-semibold' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-100' }}">
|
||||||
|
<i data-lucide="users" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>Klanten</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
<a href="{{ route('admin.makerworld.index') }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ request()->routeIs('admin.makerworld.index*') ? 'bg-slate-900 text-white font-semibold' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-100' }}">
|
<a href="{{ route('admin.makerworld.index') }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ request()->routeIs('admin.makerworld.index*') ? 'bg-slate-900 text-white font-semibold' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-100' }}">
|
||||||
<i data-lucide="download-cloud" class="w-3.5 h-3.5"></i>
|
<i data-lucide="download-cloud" class="w-3.5 h-3.5"></i>
|
||||||
<span>MakerWorld Sync</span>
|
<span>MakerWorld Sync</span>
|
||||||
@@ -139,6 +157,17 @@
|
|||||||
<a href="{{ route('admin.orders.index') }}" class="block px-3 py-2 rounded-lg {{ request()->routeIs('admin.orders.*') ? 'bg-slate-100 text-slate-900 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
|
<a href="{{ route('admin.orders.index') }}" class="block px-3 py-2 rounded-lg {{ request()->routeIs('admin.orders.*') ? 'bg-slate-100 text-slate-900 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
|
||||||
Bestellingen
|
Bestellingen
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ route('admin.communications.index') }}" class="block px-3 py-2 rounded-lg {{ request()->routeIs('admin.communications.*') ? 'bg-slate-100 text-slate-900 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
|
||||||
|
Communicatie
|
||||||
|
@if(($unreadCommCount ?? 0) > 0)
|
||||||
|
<span class="ml-2 px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-sky-500 text-white">
|
||||||
|
{{ $unreadCommCount }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('admin.customers.index') }}" class="block px-3 py-2 rounded-lg {{ request()->routeIs('admin.customers.*') ? 'bg-slate-100 text-slate-900 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
|
||||||
|
Klanten & Profielen
|
||||||
|
</a>
|
||||||
<a href="{{ route('admin.makerworld.index') }}" class="block px-3 py-2 rounded-lg {{ request()->routeIs('admin.makerworld.*') ? 'bg-slate-100 text-slate-900 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
|
<a href="{{ route('admin.makerworld.index') }}" class="block px-3 py-2 rounded-lg {{ request()->routeIs('admin.makerworld.*') ? 'bg-slate-100 text-slate-900 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
|
||||||
MakerWorld Sync
|
MakerWorld Sync
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -102,6 +102,9 @@
|
|||||||
<a href="{{ route('catalog') }}" class="px-3 py-1.5 rounded-lg transition {{ request()->routeIs('catalog') && !request('category') ? 'bg-slate-100 text-slate-900 font-semibold' : 'hover:text-slate-900 hover:bg-slate-50' }}">
|
<a href="{{ route('catalog') }}" class="px-3 py-1.5 rounded-lg transition {{ request()->routeIs('catalog') && !request('category') ? 'bg-slate-100 text-slate-900 font-semibold' : 'hover:text-slate-900 hover:bg-slate-50' }}">
|
||||||
Alle Prints
|
Alle Prints
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ route('contact.index') }}" class="px-3 py-1.5 rounded-lg transition {{ request()->routeIs('contact.*') ? 'bg-slate-100 text-slate-900 font-semibold' : 'hover:text-slate-900 hover:bg-slate-50' }}">
|
||||||
|
Vraag stellen
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -210,6 +213,9 @@
|
|||||||
<a href="{{ route('catalog') }}" class="px-3 py-2 rounded-lg hover:bg-slate-50 flex items-center justify-between text-slate-600">
|
<a href="{{ route('catalog') }}" class="px-3 py-2 rounded-lg hover:bg-slate-50 flex items-center justify-between text-slate-600">
|
||||||
<span>Alle Prints Bekijken</span>
|
<span>Alle Prints Bekijken</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ route('contact.index') }}" class="px-3 py-2 rounded-lg hover:bg-slate-50 flex items-center justify-between text-slate-900 font-medium">
|
||||||
|
<span>Vraag stellen / Contact</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
<div class="border-t border-slate-100 pt-3 mt-2">
|
<div class="border-t border-slate-100 pt-3 mt-2">
|
||||||
@guest
|
@guest
|
||||||
@@ -337,6 +343,12 @@
|
|||||||
<i data-lucide="truck" class="w-3.5 h-3.5 text-slate-400"></i>
|
<i data-lucide="truck" class="w-3.5 h-3.5 text-slate-400"></i>
|
||||||
<span>{{ setting('footer_shipping_text', 'Snelle verzending via PostNL') }}</span>
|
<span>{{ setting('footer_shipping_text', 'Snelle verzending via PostNL') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="pt-1">
|
||||||
|
<a href="{{ route('contact.index') }}" class="inline-flex items-center space-x-1 text-slate-900 font-semibold hover:underline">
|
||||||
|
<span>Direct vraag stellen</span>
|
||||||
|
<span>→</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
use App\Http\Controllers\AccountController;
|
use App\Http\Controllers\AccountController;
|
||||||
use App\Http\Controllers\Admin\CategoryController as AdminCategoryController;
|
use App\Http\Controllers\Admin\CategoryController as AdminCategoryController;
|
||||||
|
use App\Http\Controllers\Admin\CommunicationController as AdminCommunicationController;
|
||||||
use App\Http\Controllers\Admin\ContentController;
|
use App\Http\Controllers\Admin\ContentController;
|
||||||
|
use App\Http\Controllers\Admin\CustomerController as AdminCustomerController;
|
||||||
use App\Http\Controllers\Admin\MakerWorldController;
|
use App\Http\Controllers\Admin\MakerWorldController;
|
||||||
use App\Http\Controllers\Admin\OrderController as AdminOrderController;
|
use App\Http\Controllers\Admin\OrderController as AdminOrderController;
|
||||||
use App\Http\Controllers\Admin\ProductController as AdminProductController;
|
use App\Http\Controllers\Admin\ProductController as AdminProductController;
|
||||||
@@ -11,6 +13,7 @@ use App\Http\Controllers\Auth\LoginController;
|
|||||||
use App\Http\Controllers\Auth\RegisterController;
|
use App\Http\Controllers\Auth\RegisterController;
|
||||||
use App\Http\Controllers\CartController;
|
use App\Http\Controllers\CartController;
|
||||||
use App\Http\Controllers\CheckoutController;
|
use App\Http\Controllers\CheckoutController;
|
||||||
|
use App\Http\Controllers\ContactController;
|
||||||
use App\Http\Controllers\ShopController;
|
use App\Http\Controllers\ShopController;
|
||||||
use App\Models\Category;
|
use App\Models\Category;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
@@ -80,6 +83,10 @@ Route::post('/winkelwagen/bijwerken', [CartController::class, 'update'])->name('
|
|||||||
Route::delete('/winkelwagen/verwijderen/{key}', [CartController::class, 'remove'])->name('cart.remove');
|
Route::delete('/winkelwagen/verwijderen/{key}', [CartController::class, 'remove'])->name('cart.remove');
|
||||||
Route::get('/winkelwagen/aantal', [CartController::class, 'count'])->name('cart.count');
|
Route::get('/winkelwagen/aantal', [CartController::class, 'count'])->name('cart.count');
|
||||||
|
|
||||||
|
// Contact & Klantvraag stellen
|
||||||
|
Route::get('/contact', [ContactController::class, 'index'])->name('contact.index');
|
||||||
|
Route::post('/contact', [ContactController::class, 'store'])->name('contact.store')->middleware('throttle:10,1');
|
||||||
|
|
||||||
// Afrekenen & Bevestiging
|
// Afrekenen & Bevestiging
|
||||||
Route::get('/afrekenen', [CheckoutController::class, 'index'])->name('checkout.index');
|
Route::get('/afrekenen', [CheckoutController::class, 'index'])->name('checkout.index');
|
||||||
Route::post('/afrekenen', [CheckoutController::class, 'store'])->name('checkout.store')->middleware('throttle:10,1');
|
Route::post('/afrekenen', [CheckoutController::class, 'store'])->name('checkout.store')->middleware('throttle:10,1');
|
||||||
@@ -135,6 +142,17 @@ Route::prefix('admin')->name('admin.')->middleware(['auth', 'admin'])->group(fun
|
|||||||
Route::get('/orders/{order}', [AdminOrderController::class, 'show'])->name('orders.show');
|
Route::get('/orders/{order}', [AdminOrderController::class, 'show'])->name('orders.show');
|
||||||
Route::post('/orders/{order}/status', [AdminOrderController::class, 'updateStatus'])->name('orders.status');
|
Route::post('/orders/{order}/status', [AdminOrderController::class, 'updateStatus'])->name('orders.status');
|
||||||
|
|
||||||
|
// Communicatie Beheer (Vragen van klanten & antwoorden)
|
||||||
|
Route::get('/communicatie', [AdminCommunicationController::class, 'index'])->name('communications.index');
|
||||||
|
Route::get('/communicatie/{communication}', [AdminCommunicationController::class, 'show'])->name('communications.show');
|
||||||
|
Route::post('/communicatie/{communication}/status', [AdminCommunicationController::class, 'updateStatus'])->name('communications.status');
|
||||||
|
Route::post('/communicatie/{communication}/reply', [AdminCommunicationController::class, 'sendReply'])->name('communications.reply');
|
||||||
|
Route::delete('/communicatie/{communication}', [AdminCommunicationController::class, 'destroy'])->name('communications.destroy');
|
||||||
|
|
||||||
|
// Klanten & Klantprofielen Beheer
|
||||||
|
Route::get('/klanten', [AdminCustomerController::class, 'index'])->name('customers.index');
|
||||||
|
Route::get('/klanten/{customer}', [AdminCustomerController::class, 'show'])->name('customers.show');
|
||||||
|
|
||||||
// MakerWorld Importer & Collecties Auto-Sync
|
// MakerWorld Importer & Collecties Auto-Sync
|
||||||
Route::get('/makerworld', [MakerWorldController::class, 'index'])->name('makerworld.index');
|
Route::get('/makerworld', [MakerWorldController::class, 'index'])->name('makerworld.index');
|
||||||
Route::post('/makerworld/login', [MakerWorldController::class, 'login'])->name('makerworld.login');
|
Route::post('/makerworld/login', [MakerWorldController::class, 'login'])->name('makerworld.login');
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Mail\ContactInquiryConfirmationMail;
|
||||||
|
use App\Mail\ContactInquiryReceivedMail;
|
||||||
|
use App\Mail\ContactInquiryReplyMail;
|
||||||
|
use App\Models\Communication;
|
||||||
|
use App\Models\Order;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class CustomerCommunicationTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
private User $admin;
|
||||||
|
|
||||||
|
private User $customer;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->admin = User::factory()->create([
|
||||||
|
'name' => 'Admin Beheerder',
|
||||||
|
'email' => 'admin@jouwdriedee.nl',
|
||||||
|
'is_admin' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->customer = User::factory()->create([
|
||||||
|
'name' => 'Kees de Vries',
|
||||||
|
'email' => 'kees@example.com',
|
||||||
|
'is_admin' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_contact_page_loads_successfully(): void
|
||||||
|
{
|
||||||
|
$response = $this->get(route('contact.index'));
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertSee('Stel een vraag');
|
||||||
|
$response->assertSee('Directe Contactgegevens');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_customer_can_submit_question_and_both_emails_are_dispatched(): void
|
||||||
|
{
|
||||||
|
Mail::fake();
|
||||||
|
|
||||||
|
$response = $this->post(route('contact.store'), [
|
||||||
|
'name' => 'Lisa Jansen',
|
||||||
|
'email' => 'lisa@example.com',
|
||||||
|
'phone' => '06-87654321',
|
||||||
|
'subject' => 'Vraag over levertijd van Pokémon model',
|
||||||
|
'message' => 'Goedemiddag, hoe lang duurt het printen van het Charizard model in PETG?',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertRedirect(route('contact.index'));
|
||||||
|
$response->assertSessionHas('success');
|
||||||
|
|
||||||
|
// Check database
|
||||||
|
$this->assertDatabaseHas('communications', [
|
||||||
|
'name' => 'Lisa Jansen',
|
||||||
|
'email' => 'lisa@example.com',
|
||||||
|
'subject' => 'Vraag over levertijd van Pokémon model',
|
||||||
|
'status' => 'new',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$communication = Communication::where('email', 'lisa@example.com')->first();
|
||||||
|
$this->assertNotNull($communication);
|
||||||
|
|
||||||
|
// 1. Mail to webshop management (info@jouwdrieedee.nl / info@jouwdriedee.nl)
|
||||||
|
Mail::assertSent(ContactInquiryReceivedMail::class, function ($mail) use ($communication) {
|
||||||
|
return $mail->communication->id === $communication->id;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Afschrift to customer from (noreply@jouwdriedee.nl)
|
||||||
|
Mail::assertSent(ContactInquiryConfirmationMail::class, function ($mail) use ($communication) {
|
||||||
|
$hasCorrectRecipient = $mail->hasTo('lisa@example.com');
|
||||||
|
$hasCorrectFrom = $mail->hasFrom('noreply@jouwdriedee.nl');
|
||||||
|
|
||||||
|
return $hasCorrectRecipient && $hasCorrectFrom && $mail->communication->id === $communication->id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_logged_in_customer_inquiry_is_linked_and_visible_in_account(): void
|
||||||
|
{
|
||||||
|
Mail::fake();
|
||||||
|
|
||||||
|
$response = $this->actingAs($this->customer)->post(route('contact.store'), [
|
||||||
|
'name' => $this->customer->name,
|
||||||
|
'email' => $this->customer->email,
|
||||||
|
'subject' => 'Speciale kleur vraag',
|
||||||
|
'message' => 'Kan het model ook in glow-in-the-dark filament worden geprint?',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertRedirect(route('contact.index'));
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('communications', [
|
||||||
|
'user_id' => $this->customer->id,
|
||||||
|
'email' => 'kees@example.com',
|
||||||
|
'subject' => 'Speciale kleur vraag',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Customer dashboard in /mijn-account shows their question
|
||||||
|
$accountResponse = $this->actingAs($this->customer)->get(route('account.index'));
|
||||||
|
$accountResponse->assertStatus(200);
|
||||||
|
$accountResponse->assertSee('Speciale kleur vraag');
|
||||||
|
$accountResponse->assertSee('Mijn Vragen');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_admin_can_view_communications_in_dedicated_admin_section(): void
|
||||||
|
{
|
||||||
|
$comm = Communication::create([
|
||||||
|
'name' => 'Peter Bakker',
|
||||||
|
'email' => 'peter@example.com',
|
||||||
|
'subject' => 'Garantie op 3D prints',
|
||||||
|
'message' => 'Wat voor garantie zit er op technische 3D prints?',
|
||||||
|
'status' => 'new',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Admin list
|
||||||
|
$indexResponse = $this->actingAs($this->admin)->get(route('admin.communications.index'));
|
||||||
|
$indexResponse->assertStatus(200);
|
||||||
|
$indexResponse->assertSee('Communicatie');
|
||||||
|
$indexResponse->assertSee('Garantie op 3D prints');
|
||||||
|
$indexResponse->assertSee('Peter Bakker');
|
||||||
|
|
||||||
|
// Admin detail
|
||||||
|
$showResponse = $this->actingAs($this->admin)->get(route('admin.communications.show', $comm));
|
||||||
|
$showResponse->assertStatus(200);
|
||||||
|
$showResponse->assertSee('Garantie op 3D prints');
|
||||||
|
$showResponse->assertSee('Wat voor garantie zit er op technische 3D prints?');
|
||||||
|
|
||||||
|
// Status is automatically transitioned from new to in_progress upon opening
|
||||||
|
$this->assertEquals('in_progress', $comm->fresh()->status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_admin_can_reply_to_customer_from_admin_section(): void
|
||||||
|
{
|
||||||
|
Mail::fake();
|
||||||
|
|
||||||
|
$comm = Communication::create([
|
||||||
|
'name' => 'Sophie Visser',
|
||||||
|
'email' => 'sophie@example.com',
|
||||||
|
'subject' => 'Custom formaat mogelijk?',
|
||||||
|
'message' => 'Kan de plantenbak ook 30cm hoog geprint worden?',
|
||||||
|
'status' => 'in_progress',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$replyResponse = $this->actingAs($this->admin)->post(route('admin.communications.reply', $comm), [
|
||||||
|
'message' => 'Beste Sophie, ja zeker! We kunnen dit model opschalen naar 30cm. De levertijd is 3 dagen.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$replyResponse->assertRedirect();
|
||||||
|
$replyResponse->assertSessionHas('success');
|
||||||
|
|
||||||
|
// Verify status and reply in database
|
||||||
|
$this->assertEquals('resolved', $comm->fresh()->status);
|
||||||
|
$this->assertNotNull($comm->fresh()->last_replied_at);
|
||||||
|
$this->assertDatabaseHas('communication_replies', [
|
||||||
|
'communication_id' => $comm->id,
|
||||||
|
'is_admin_reply' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify email was sent to customer from info@jouwdriedee.nl
|
||||||
|
Mail::assertSent(ContactInquiryReplyMail::class, function ($mail) {
|
||||||
|
return $mail->hasTo('sophie@example.com') && $mail->hasFrom('info@jouwdriedee.nl');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_admin_can_view_communication_inside_customer_profile(): void
|
||||||
|
{
|
||||||
|
// Link inquiry to customer
|
||||||
|
$comm = Communication::create([
|
||||||
|
'user_id' => $this->customer->id,
|
||||||
|
'name' => $this->customer->name,
|
||||||
|
'email' => $this->customer->email,
|
||||||
|
'subject' => 'Vraag binnen profiel',
|
||||||
|
'message' => 'Dit is een vraag die in het klantprofiel zichtbaar moet zijn.',
|
||||||
|
'status' => 'new',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Also create an order for this customer
|
||||||
|
Order::create([
|
||||||
|
'user_id' => $this->customer->id,
|
||||||
|
'order_number' => 'ORD-TEST-999',
|
||||||
|
'customer_name' => $this->customer->name,
|
||||||
|
'customer_email' => $this->customer->email,
|
||||||
|
'shipping_street' => 'Dorpsstraat',
|
||||||
|
'shipping_number' => '1',
|
||||||
|
'shipping_postal_code' => '1234 AB',
|
||||||
|
'shipping_city' => 'Utrecht',
|
||||||
|
'subtotal' => 45.00,
|
||||||
|
'shipping_cost' => 0.00,
|
||||||
|
'total' => 45.00,
|
||||||
|
'status' => 'pending',
|
||||||
|
'payment_status' => 'paid',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 1. Customers overview in admin
|
||||||
|
$indexResponse = $this->actingAs($this->admin)->get(route('admin.customers.index'));
|
||||||
|
$indexResponse->assertStatus(200);
|
||||||
|
$indexResponse->assertSee($this->customer->name);
|
||||||
|
$indexResponse->assertSee($this->customer->email);
|
||||||
|
|
||||||
|
// 2. Customer profile in admin (by user ID)
|
||||||
|
$profileResponse = $this->actingAs($this->admin)->get(route('admin.customers.show', $this->customer->id));
|
||||||
|
$profileResponse->assertStatus(200);
|
||||||
|
$profileResponse->assertSee('Klantprofiel: '.$this->customer->name);
|
||||||
|
$profileResponse->assertSee('Communicatie & Vragen', false);
|
||||||
|
$profileResponse->assertSee('Vraag binnen profiel');
|
||||||
|
$profileResponse->assertSee('ORD-TEST-999');
|
||||||
|
|
||||||
|
// 3. Customer profile in admin by email
|
||||||
|
$profileByEmail = $this->actingAs($this->admin)->get(route('admin.customers.show', $this->customer->email));
|
||||||
|
$profileByEmail->assertStatus(200);
|
||||||
|
$profileByEmail->assertSee('Vraag binnen profiel');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_guest_and_non_admin_cannot_access_admin_communications_or_customers(): void
|
||||||
|
{
|
||||||
|
// Guests redirected to login
|
||||||
|
$this->get(route('admin.communications.index'))->assertRedirect(route('login'));
|
||||||
|
$this->get(route('admin.customers.index'))->assertRedirect(route('login'));
|
||||||
|
|
||||||
|
// Normal customer is redirected to home with error flash
|
||||||
|
$this->actingAs($this->customer)->get(route('admin.communications.index'))->assertRedirect(route('home'));
|
||||||
|
$this->actingAs($this->customer)->get(route('admin.customers.index'))->assertRedirect(route('home'));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user