added club create and edit modal
This commit is contained in:
1 parent
a4bc983660
commit
da688375e3
38 files changed
+9194
-44
No files matched your search
@@ -7,7 +7,7 @@
|
||||
<h2 class="h3 fw-bold mb-1">Club Details</h2>
|
||||
<p class="text-muted mb-0">Manage your club information</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#editClubModal">
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#clubModal">
|
||||
<i class="bi bi-pencil me-2"></i>Edit Details
|
||||
</button>
|
||||
</div>
|
||||
@@ -124,4 +124,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include Club Modal in Edit Mode -->
|
||||
<x-club-modal mode="edit" :club="$club" />
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,266 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('admin-content')
|
||||
<div>
|
||||
<!-- Page Header -->
|
||||
<div class="mb-4">
|
||||
<h1 class="h2 fw-bold mb-2">All Clubs</h1>
|
||||
<p class="text-muted">Manage all clubs on the platform</p>
|
||||
</div>
|
||||
|
||||
<!-- Search and Actions Bar -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div class="flex-grow-1 me-3">
|
||||
<input type="text" id="clubSearch" class="form-control" placeholder="Search clubs by name, location, or description..." value="{{ $search ?? '' }}">
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#clubModal" onclick="openClubModal('create')">
|
||||
<i class="bi bi-plus-circle me-2"></i>Add New Club
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Clubs Grid -->
|
||||
@if($clubs->count() > 0)
|
||||
<div class="row g-4 mb-4" id="clubsGrid">
|
||||
@foreach($clubs as $club)
|
||||
<div class="col-md-6 col-xl-4 club-card-wrapper"
|
||||
data-club-name="{{ $club->club_name }}"
|
||||
data-club-address="{{ $club->address ?? '' }}"
|
||||
data-club-owner="{{ $club->owner->full_name ?? '' }}">
|
||||
<div class="card border shadow-sm overflow-hidden club-card" style="border-radius: 0; cursor: pointer; transition: all 0.3s ease;">
|
||||
<!-- Cover Image -->
|
||||
<div class="position-relative overflow-hidden" style="height: 192px;" onclick="window.location.href='{{ route('admin.club.dashboard', $club) }}'">
|
||||
@if($club->cover_image)
|
||||
<img src="{{ asset('storage/' . $club->cover_image) }}" alt="{{ $club->club_name }}" loading="lazy" class="w-100 h-100 club-cover-img" style="object-fit: cover; transition: transform 0.3s ease;">
|
||||
@else
|
||||
<div class="w-100 h-100 d-flex align-items-center justify-content-center" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||||
<i class="bi bi-image text-white" style="font-size: 3rem; opacity: 0.3;"></i>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Club Logo - Bottom Left -->
|
||||
<div class="position-absolute" style="bottom: 8px; left: 8px;">
|
||||
<div class="bg-white shadow border p-0.5" style="width: 80px; height: 80px; border-radius: 50%; border-color: rgba(0,0,0,0.1) !important;">
|
||||
@if($club->logo)
|
||||
<img src="{{ asset('storage/' . $club->logo) }}" alt="{{ $club->club_name }} logo" loading="lazy" class="w-100 h-100 rounded-circle" style="object-fit: contain;">
|
||||
@else
|
||||
<div class="w-100 h-100 rounded-circle bg-primary d-flex align-items-center justify-content-center">
|
||||
<span class="text-white fw-bold fs-4">{{ substr($club->club_name, 0, 1) }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Admin Badge - Top Left -->
|
||||
<div class="position-absolute" style="top: 8px; left: 8px;">
|
||||
<span class="badge text-white px-3 py-1" style="background-color: rgba(147, 51, 234, 0.9); border-radius: 9999px; font-size: 0.75rem; font-weight: 600;">Admin</span>
|
||||
</div>
|
||||
|
||||
<!-- Edit Button - Top Right -->
|
||||
<div class="position-absolute" style="top: 8px; right: 8px;">
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-light shadow-sm"
|
||||
onclick="event.stopPropagation(); openClubModal('edit', {{ $club->id }})"
|
||||
title="Edit Club">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Body -->
|
||||
<div class="p-4" style="background-color: white;" onclick="window.location.href='{{ route('admin.club.dashboard', $club) }}'">
|
||||
<div class="mb-3">
|
||||
<!-- Club Name -->
|
||||
<h3 class="fw-semibold mb-2 club-title" style="font-size: 1.125rem; color: #1f2937; transition: color 0.3s ease;">{{ $club->club_name }}</h3>
|
||||
|
||||
<!-- Address -->
|
||||
@if($club->address)
|
||||
<div class="d-flex align-items-center text-muted" style="font-size: 0.875rem;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="me-1 flex-shrink-0">
|
||||
<path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"></path>
|
||||
<circle cx="12" cy="10" r="3"></circle>
|
||||
</svg>
|
||||
<span class="text-truncate">{{ $club->address }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Stats Grid -->
|
||||
<div class="row g-2 text-center" style="font-size: 0.75rem;">
|
||||
<div class="col-4">
|
||||
<div class="p-2 rounded" style="background-color: rgba(147, 51, 234, 0.05);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mx-auto mb-1" style="color: hsl(var(--primary));">
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="9" cy="7" r="4"></circle>
|
||||
<path d="M22 21v-2a4 4 0 0 0-3-3.87"></path>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
||||
</svg>
|
||||
<p class="fw-semibold mb-0" style="color: #1f2937;">{{ $club->members_count }}</p>
|
||||
<p class="text-muted mb-0">Members</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="p-2 rounded" style="background-color: rgba(147, 51, 234, 0.05);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mx-auto mb-1" style="color: hsl(var(--primary));">
|
||||
<path d="M14.4 14.4 9.6 9.6"></path>
|
||||
<path d="M18.657 21.485a2 2 0 1 1-2.829-2.828l-1.767 1.768a2 2 0 1 1-2.829-2.829l6.364-6.364a2 2 0 1 1 2.829 2.829l-1.768 1.767a2 2 0 1 1 2.828 2.829z"></path>
|
||||
<path d="m21.5 21.5-1.4-1.4"></path>
|
||||
<path d="M3.9 3.9 2.5 2.5"></path>
|
||||
<path d="M6.404 12.768a2 2 0 1 1-2.829-2.829l1.768-1.767a2 2 0 1 1-2.828-2.829l2.828-2.828a2 2 0 1 1 2.829 2.828l1.767-1.768a2 2 0 1 1 2.829 2.829z"></path>
|
||||
</svg>
|
||||
<p class="fw-semibold mb-0" style="color: #1f2937;">{{ $club->packages_count }}</p>
|
||||
<p class="text-muted mb-0">Packages</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="p-2 rounded" style="background-color: rgba(147, 51, 234, 0.05);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mx-auto mb-1" style="color: hsl(var(--primary));">
|
||||
<path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
|
||||
</svg>
|
||||
<p class="fw-semibold mb-0" style="color: #1f2937;">{{ $club->instructors_count }}</p>
|
||||
<p class="text-muted mb-0">Trainers</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="d-flex justify-content-center mb-4">
|
||||
{{ $clubs->links() }}
|
||||
</div>
|
||||
@else
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body text-center py-5">
|
||||
<i class="bi bi-building text-muted" style="font-size: 4rem;"></i>
|
||||
<h5 class="mt-3 mb-2">No Clubs Found</h5>
|
||||
<p class="text-muted mb-4">
|
||||
@if($search)
|
||||
No clubs match your search criteria.
|
||||
@else
|
||||
Get started by creating your first club.
|
||||
@endif
|
||||
</p>
|
||||
@if(!$search)
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#clubModal" onclick="openClubModal('create')">
|
||||
<i class="bi bi-plus-circle me-2"></i>Add New Club
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Include Club Modal -->
|
||||
<x-club-modal mode="create" />
|
||||
|
||||
<!-- Include User Picker Modal -->
|
||||
<x-user-picker-modal />
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.club-card {
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.club-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04) !important;
|
||||
}
|
||||
|
||||
.club-card:hover .club-cover-img {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.club-card:hover .club-title {
|
||||
color: hsl(var(--primary)) !important;
|
||||
}
|
||||
|
||||
.club-card-wrapper {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.club-card-wrapper.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Real-time search filtering
|
||||
document.getElementById('clubSearch').addEventListener('input', function(e) {
|
||||
const searchTerm = e.target.value.toLowerCase();
|
||||
const clubCards = document.querySelectorAll('.club-card-wrapper');
|
||||
|
||||
clubCards.forEach(function(card) {
|
||||
const clubName = card.getAttribute('data-club-name').toLowerCase();
|
||||
const clubAddress = card.getAttribute('data-club-address').toLowerCase();
|
||||
const clubOwner = card.getAttribute('data-club-owner').toLowerCase();
|
||||
|
||||
if (clubName.includes(searchTerm) || clubAddress.includes(searchTerm) || clubOwner.includes(searchTerm)) {
|
||||
card.classList.remove('hidden');
|
||||
} else {
|
||||
card.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Open club modal
|
||||
async function openClubModal(mode, clubId = null) {
|
||||
const modal = document.getElementById('clubModal');
|
||||
const form = document.getElementById('clubForm');
|
||||
|
||||
if (!modal || !form) return;
|
||||
|
||||
// Set mode
|
||||
form.dataset.mode = mode;
|
||||
form.dataset.clubId = clubId || '';
|
||||
|
||||
// Update modal title
|
||||
const modalTitle = modal.querySelector('.modal-title');
|
||||
if (modalTitle) {
|
||||
modalTitle.textContent = mode === 'edit' ? 'Edit Club' : 'Create New Club';
|
||||
}
|
||||
|
||||
// Update submit button text
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
if (submitBtn) {
|
||||
submitBtn.innerHTML = mode === 'edit'
|
||||
? '<i class="bi bi-check-circle me-2"></i>Update Club'
|
||||
: '<i class="bi bi-check-circle me-2"></i>Create Club';
|
||||
}
|
||||
|
||||
// If edit mode, load club data
|
||||
if (mode === 'edit' && clubId) {
|
||||
try {
|
||||
const response = await fetch(`/admin/api/clubs/${clubId}`);
|
||||
if (response.ok) {
|
||||
const club = await response.json();
|
||||
populateFormWithClubData(club);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading club data:', error);
|
||||
}
|
||||
} else {
|
||||
// Reset form for create mode
|
||||
form.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// Populate form with club data (for edit mode)
|
||||
function populateFormWithClubData(club) {
|
||||
// This would populate all form fields with club data
|
||||
// Implementation depends on the exact structure
|
||||
console.log('Loading club data:', club);
|
||||
|
||||
// Example: populate basic fields
|
||||
if (club.club_name) document.getElementById('club_name').value = club.club_name;
|
||||
if (club.slug) document.getElementById('slug').value = club.slug;
|
||||
// ... populate other fields
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
@@ -13,9 +13,9 @@
|
||||
<div class="flex-grow-1 me-3">
|
||||
<input type="text" id="clubSearch" class="form-control" placeholder="Search clubs by name, location, or description..." value="{{ $search ?? '' }}">
|
||||
</div>
|
||||
<a href="{{ route('admin.platform.clubs.create') }}" class="btn btn-primary">
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#clubModal" onclick="openClubModal('create')">
|
||||
<i class="bi bi-plus-circle me-2"></i>Add New Club
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Clubs Grid -->
|
||||
@@ -51,12 +51,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Admin Badge - Top Left -->
|
||||
<div class="position-absolute" style="top: 8px; left: 8px;">
|
||||
<span class="badge text-white px-3 py-1" style="background-color: rgba(147, 51, 234, 0.9); border-radius: 9999px; font-size: 0.75rem; font-weight: 600;">Admin</span>
|
||||
</div>
|
||||
<!-- Admin Badge - Top Left -->
|
||||
<div class="position-absolute" style="top: 8px; left: 8px;">
|
||||
<span class="badge text-white px-3 py-1" style="background-color: rgba(147, 51, 234, 0.9); border-radius: 9999px; font-size: 0.75rem; font-weight: 600;">Admin</span>
|
||||
</div>
|
||||
|
||||
<!-- Edit Button - Top Right -->
|
||||
<div class="position-absolute" style="top: 8px; right: 8px;">
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-light shadow-sm"
|
||||
onclick="event.stopPropagation(); openClubModal('edit', {{ $club->id }})"
|
||||
title="Edit Club">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Body -->
|
||||
<div class="p-4" style="background-color: white;">
|
||||
<div class="mb-3">
|
||||
@@ -136,15 +146,18 @@
|
||||
@endif
|
||||
</p>
|
||||
@if(!$search)
|
||||
<a href="{{ route('admin.platform.clubs.create') }}" class="btn btn-primary">
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#clubModal" onclick="openClubModal('create')">
|
||||
<i class="bi bi-plus-circle me-2"></i>Add New Club
|
||||
</a>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Include Club Modal -->
|
||||
<x-club-modal mode="create" />
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.club-card {
|
||||
@@ -193,6 +206,87 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Open club modal
|
||||
async function openClubModal(mode, clubId = null) {
|
||||
const modal = document.getElementById('clubModal');
|
||||
const form = document.getElementById('clubForm');
|
||||
|
||||
if (!modal || !form) return;
|
||||
|
||||
// Set mode
|
||||
form.dataset.mode = mode;
|
||||
form.dataset.clubId = clubId || '';
|
||||
|
||||
// Update modal title
|
||||
const modalTitle = modal.querySelector('.modal-title');
|
||||
if (modalTitle) {
|
||||
modalTitle.textContent = mode === 'edit' ? 'Edit Club' : 'Create New Club';
|
||||
}
|
||||
|
||||
// Update submit button text
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
if (submitBtn) {
|
||||
submitBtn.innerHTML = mode === 'edit'
|
||||
? '<i class="bi bi-check-circle me-2"></i>Update Club'
|
||||
: '<i class="bi bi-check-circle me-2"></i>Create Club';
|
||||
}
|
||||
|
||||
// If edit mode, load club data
|
||||
if (mode === 'edit' && clubId) {
|
||||
try {
|
||||
const response = await fetch(`/admin/api/clubs/${clubId}`);
|
||||
if (response.ok) {
|
||||
const club = await response.json();
|
||||
populateFormWithClubData(club);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading club data:', error);
|
||||
}
|
||||
} else {
|
||||
// Reset form for create mode
|
||||
form.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// Populate form with club data (for edit mode)
|
||||
function populateFormWithClubData(club) {
|
||||
console.log('Loading club data:', club);
|
||||
|
||||
// Populate basic fields
|
||||
if (club.club_name) document.getElementById('club_name').value = club.club_name;
|
||||
if (club.slug) document.getElementById('slug').value = club.slug;
|
||||
if (club.slogan) document.getElementById('slogan').value = club.slogan;
|
||||
if (club.description) document.getElementById('description').value = club.description;
|
||||
if (club.established_date) document.getElementById('established_date').value = club.established_date;
|
||||
if (club.commercial_reg_number) document.getElementById('commercial_reg_number').value = club.commercial_reg_number;
|
||||
if (club.vat_reg_number) document.getElementById('vat_reg_number').value = club.vat_reg_number;
|
||||
if (club.vat_percentage) document.getElementById('vat_percentage').value = club.vat_percentage;
|
||||
|
||||
// Populate owner
|
||||
if (club.owner_user_id) document.getElementById('owner_user_id').value = club.owner_user_id;
|
||||
|
||||
// Populate location fields
|
||||
if (club.country) document.getElementById('country').value = club.country;
|
||||
if (club.timezone) document.getElementById('timezone').value = club.timezone;
|
||||
if (club.currency) document.getElementById('currency').value = club.currency;
|
||||
if (club.address) document.getElementById('address').value = club.address;
|
||||
if (club.gps_lat) document.getElementById('gps_lat').value = club.gps_lat;
|
||||
if (club.gps_long) document.getElementById('gps_long').value = club.gps_long;
|
||||
|
||||
// Populate contact fields
|
||||
if (club.email) {
|
||||
document.getElementById('email_option_custom').checked = true;
|
||||
document.getElementById('email').value = club.email;
|
||||
}
|
||||
|
||||
// Populate finance fields
|
||||
if (club.enrollment_fee) document.getElementById('enrollment_fee').value = club.enrollment_fee;
|
||||
if (club.status) document.getElementById('club_status').value = club.status;
|
||||
if (club.public_profile_enabled !== undefined) {
|
||||
document.getElementById('public_profile_enabled').checked = club.public_profile_enabled;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
@@ -0,0 +1,698 @@
|
||||
@props(['mode' => 'create', 'club' => null])
|
||||
|
||||
@php
|
||||
$isEdit = $mode === 'edit' && $club;
|
||||
$modalId = 'clubModal';
|
||||
$modalTitle = $isEdit ? 'Edit Club' : 'Create New Club';
|
||||
@endphp
|
||||
|
||||
<!-- Club Modal -->
|
||||
<div class="modal fade" id="{{ $modalId }}" tabindex="-1" aria-labelledby="{{ $modalId }}Label" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||||
<div class="modal-dialog modal-dialog-centered modal-xl">
|
||||
<div class="modal-content" style="border-radius: 1rem; border: none; max-height: 90vh; display: flex; flex-direction: column;">
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header border-0 pb-0" style="padding: 1.5rem 1.5rem 0;">
|
||||
<div class="w-100">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<div>
|
||||
<h4 class="modal-title fw-bold mb-1" id="{{ $modalId }}Label">{{ $modalTitle }}</h4>
|
||||
<p class="text-muted small mb-0">Fill in the information across all tabs</p>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<!-- Progress Indicator -->
|
||||
<div class="d-flex align-items-center gap-2 mb-3">
|
||||
<span class="badge bg-primary" id="stepIndicator">Step 1 of 5</span>
|
||||
<div class="progress flex-grow-1" style="height: 6px;">
|
||||
<div class="progress-bar" id="progressBar" role="progressbar" style="width: 20%;" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<ul class="nav nav-tabs border-0" id="clubModalTabs" role="tablist" style="gap: 0.5rem; flex-wrap: nowrap; overflow-x: auto;">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="basic-tab" data-bs-toggle="tab" data-bs-target="#basic" type="button" role="tab" aria-controls="basic" aria-selected="true" data-step="1">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
<span class="d-none d-md-inline">Basic Info</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="identity-tab" data-bs-toggle="tab" data-bs-target="#identity" type="button" role="tab" aria-controls="identity" aria-selected="false" data-step="2">
|
||||
<i class="bi bi-palette me-2"></i>
|
||||
<span class="d-none d-md-inline">Identity & Branding</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="location-tab" data-bs-toggle="tab" data-bs-target="#location" type="button" role="tab" aria-controls="location" aria-selected="false" data-step="3">
|
||||
<i class="bi bi-geo-alt me-2"></i>
|
||||
<span class="d-none d-md-inline">Location</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="contact-tab" data-bs-toggle="tab" data-bs-target="#contact" type="button" role="tab" aria-controls="contact" aria-selected="false" data-step="4">
|
||||
<i class="bi bi-telephone me-2"></i>
|
||||
<span class="d-none d-md-inline">Contact</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="finance-tab" data-bs-toggle="tab" data-bs-target="#finance" type="button" role="tab" aria-controls="finance" aria-selected="false" data-step="5">
|
||||
<i class="bi bi-bank me-2"></i>
|
||||
<span class="d-none d-md-inline">Finance & Settings</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body (Scrollable) -->
|
||||
<div class="modal-body" style="padding: 1.5rem; overflow-y: auto; flex: 1;">
|
||||
<form id="clubForm" data-mode="{{ $mode }}" data-club-id="{{ $club->id ?? '' }}">
|
||||
@csrf
|
||||
@if($isEdit)
|
||||
@method('PUT')
|
||||
@endif
|
||||
|
||||
<div class="tab-content" id="clubModalTabContent">
|
||||
<!-- Tab 1: Basic Information -->
|
||||
<div class="tab-pane fade show active" id="basic" role="tabpanel" aria-labelledby="basic-tab">
|
||||
<x-club-modal.tabs.basic-info :club="$club" :mode="$mode" />
|
||||
</div>
|
||||
|
||||
<!-- Tab 2: Identity & Branding -->
|
||||
<div class="tab-pane fade" id="identity" role="tabpanel" aria-labelledby="identity-tab">
|
||||
<x-club-modal.tabs.identity-branding :club="$club" :mode="$mode" />
|
||||
</div>
|
||||
|
||||
<!-- Tab 3: Location -->
|
||||
<div class="tab-pane fade" id="location" role="tabpanel" aria-labelledby="location-tab">
|
||||
<x-club-modal.tabs.location :club="$club" :mode="$mode" />
|
||||
</div>
|
||||
|
||||
<!-- Tab 4: Contact -->
|
||||
<div class="tab-pane fade" id="contact" role="tabpanel" aria-labelledby="contact-tab">
|
||||
<x-club-modal.tabs.contact :club="$club" :mode="$mode" />
|
||||
</div>
|
||||
|
||||
<!-- Tab 5: Finance & Settings -->
|
||||
<div class="tab-pane fade" id="finance" role="tabpanel" aria-labelledby="finance-tab">
|
||||
<x-club-modal.tabs.finance-settings :club="$club" :mode="$mode" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<div class="modal-footer border-0" style="padding: 1rem 1.5rem 1.5rem; gap: 0.75rem;">
|
||||
<button type="button" class="btn btn-secondary" id="prevBtn" style="display: none;">
|
||||
<i class="bi bi-arrow-left me-2"></i>Back
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="nextBtn">
|
||||
Next<i class="bi bi-arrow-right ms-2"></i>
|
||||
</button>
|
||||
<button type="button" class="btn text-white" id="submitBtn" style="display: none; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||||
<i class="bi bi-check-circle me-2"></i>{{ $isEdit ? 'Update Club' : 'Create Club' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
/* Club Modal Custom Styles */
|
||||
#clubModal .nav-tabs {
|
||||
border-bottom: 2px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
#clubModal .nav-tabs .nav-link {
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-weight: 500;
|
||||
padding: 0.75rem 1rem;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#clubModal .nav-tabs .nav-link:hover {
|
||||
color: hsl(var(--primary));
|
||||
border-bottom-color: hsl(var(--primary) / 0.3);
|
||||
}
|
||||
|
||||
#clubModal .nav-tabs .nav-link.active {
|
||||
color: hsl(var(--primary));
|
||||
border-bottom-color: hsl(var(--primary));
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#clubModal .nav-tabs .nav-link i {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
#clubModal .modal-body {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: hsl(var(--border)) transparent;
|
||||
}
|
||||
|
||||
#clubModal .modal-body::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
#clubModal .modal-body::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#clubModal .modal-body::-webkit-scrollbar-thumb {
|
||||
background-color: hsl(var(--border));
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#clubModal .form-label {
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#clubModal .form-control:focus,
|
||||
#clubModal .form-select:focus {
|
||||
border-color: hsl(var(--primary));
|
||||
box-shadow: 0 0 0 0.2rem hsl(var(--primary) / 0.15);
|
||||
}
|
||||
|
||||
#clubModal .tab-pane {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ISSUE 1 FIX: Internal User Picker Overlay Styles */
|
||||
.user-picker-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1060;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.user-picker-panel {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.user-picker-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.user-picker-body {
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-picker-item {
|
||||
padding: 1rem;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.user-picker-item:hover {
|
||||
border-color: hsl(var(--primary));
|
||||
background-color: hsl(var(--muted) / 0.3);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
#clubModal .modal-xl {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
#clubModal .nav-tabs .nav-link span {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#clubModal .nav-tabs .nav-link {
|
||||
padding: 0.75rem 0.5rem;
|
||||
}
|
||||
|
||||
.user-picker-panel {
|
||||
max-height: 90vh;
|
||||
}
|
||||
}
|
||||
|
||||
/* ISSUE 4 FIX: Map container styles */
|
||||
#clubMap {
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
border-radius: 0.5rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Hide Leaflet attribution */
|
||||
.leaflet-control-attribution {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/qrcode.min.js"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
|
||||
<script>
|
||||
// ISSUE 1 FIX: Internal User Picker Functions
|
||||
let allUsersData = [];
|
||||
|
||||
async function showUserPicker() {
|
||||
const overlay = document.getElementById('userPickerOverlay');
|
||||
if (overlay) {
|
||||
overlay.style.display = 'flex';
|
||||
// Load users
|
||||
await loadUsersInternal();
|
||||
// Focus on search input
|
||||
document.getElementById('userSearchInputInternal')?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function hideUserPicker() {
|
||||
const overlay = document.getElementById('userPickerOverlay');
|
||||
if (overlay) {
|
||||
overlay.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUsersInternal() {
|
||||
const loadingDiv = document.getElementById('userPickerLoadingInternal');
|
||||
const resultsDiv = document.getElementById('userPickerResultsInternal');
|
||||
const noResultsDiv = document.getElementById('userPickerNoResultsInternal');
|
||||
|
||||
if (loadingDiv) loadingDiv.style.display = 'block';
|
||||
if (resultsDiv) resultsDiv.innerHTML = '';
|
||||
if (noResultsDiv) noResultsDiv.style.display = 'none';
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/api/users');
|
||||
if (response.ok) {
|
||||
allUsersData = await response.json();
|
||||
displayUsersInternal(allUsersData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading users:', error);
|
||||
} finally {
|
||||
if (loadingDiv) loadingDiv.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function displayUsersInternal(users) {
|
||||
const resultsDiv = document.getElementById('userPickerResultsInternal');
|
||||
const noResultsDiv = document.getElementById('userPickerNoResultsInternal');
|
||||
|
||||
if (!resultsDiv) return;
|
||||
|
||||
if (users.length === 0) {
|
||||
resultsDiv.innerHTML = '';
|
||||
if (noResultsDiv) noResultsDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
if (noResultsDiv) noResultsDiv.style.display = 'none';
|
||||
|
||||
resultsDiv.innerHTML = users.map(user => `
|
||||
<div class="user-picker-item" onclick="selectUserInternal(${user.id}, '${user.full_name}', '${user.email}', '${user.mobile_formatted || ''}', '${user.profile_picture || ''}')">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
${user.profile_picture
|
||||
? `<img src="/storage/${user.profile_picture}" alt="${user.full_name}" class="rounded-circle" style="width: 50px; height: 50px; object-fit: cover;">`
|
||||
: `<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center" style="width: 50px; height: 50px; font-size: 1.25rem; font-weight: 600;">${user.full_name.charAt(0)}</div>`
|
||||
}
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-semibold">${user.full_name}</div>
|
||||
<div class="small text-muted">
|
||||
<i class="bi bi-envelope me-1"></i>${user.email}
|
||||
${user.mobile_formatted ? `<span class="ms-2"><i class="bi bi-phone me-1"></i>${user.mobile_formatted}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function selectUserInternal(id, name, email, mobile, picture) {
|
||||
// Set hidden input
|
||||
const ownerInput = document.getElementById('owner_user_id');
|
||||
if (ownerInput) {
|
||||
ownerInput.value = id;
|
||||
ownerInput.dispatchEvent(new Event('change'));
|
||||
}
|
||||
|
||||
// Update display
|
||||
const ownerDisplay = document.getElementById('ownerDisplay');
|
||||
if (ownerDisplay) {
|
||||
ownerDisplay.innerHTML = `
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
${picture
|
||||
? `<img src="/storage/${picture}" alt="${name}" class="rounded-circle" style="width: 50px; height: 50px; object-fit: cover;">`
|
||||
: `<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center" style="width: 50px; height: 50px; font-size: 1.25rem; font-weight: 600;">${name.charAt(0)}</div>`
|
||||
}
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-semibold">${name}</div>
|
||||
<div class="small text-muted">
|
||||
<i class="bi bi-envelope me-1"></i>${email}
|
||||
${mobile ? `<span class="ms-2"><i class="bi bi-phone me-1"></i>${mobile}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Hide picker
|
||||
hideUserPicker();
|
||||
}
|
||||
|
||||
// Search functionality for internal user picker
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const searchInput = document.getElementById('userSearchInputInternal');
|
||||
if (searchInput) {
|
||||
let searchTimeout;
|
||||
searchInput.addEventListener('input', function() {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
const searchTerm = this.value.toLowerCase();
|
||||
const filtered = allUsersData.filter(user =>
|
||||
user.full_name.toLowerCase().includes(searchTerm) ||
|
||||
user.email.toLowerCase().includes(searchTerm) ||
|
||||
(user.mobile_formatted && user.mobile_formatted.includes(searchTerm))
|
||||
);
|
||||
displayUsersInternal(filtered);
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Club Modal Controller
|
||||
(function() {
|
||||
const modal = document.getElementById('clubModal');
|
||||
if (!modal) return;
|
||||
|
||||
const form = document.getElementById('clubForm');
|
||||
const tabs = ['basic', 'identity', 'location', 'contact', 'finance'];
|
||||
let currentTab = 0;
|
||||
let draftLoaded = false; // ISSUE 2 & 5 FIX: Track if draft was loaded
|
||||
let toastShown = {}; // ISSUE 5 FIX: Track shown toasts per tab
|
||||
|
||||
const prevBtn = document.getElementById('prevBtn');
|
||||
const nextBtn = document.getElementById('nextBtn');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const stepIndicator = document.getElementById('stepIndicator');
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
|
||||
// Initialize
|
||||
function init() {
|
||||
updateButtons();
|
||||
attachEventListeners();
|
||||
// ISSUE 2 & 5 FIX: Load draft only once on modal open
|
||||
if (!draftLoaded && form.dataset.mode === 'create') {
|
||||
loadDraft();
|
||||
draftLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update button visibility and progress
|
||||
function updateButtons() {
|
||||
prevBtn.style.display = currentTab === 0 ? 'none' : 'inline-block';
|
||||
nextBtn.style.display = currentTab === tabs.length - 1 ? 'none' : 'inline-block';
|
||||
submitBtn.style.display = currentTab === tabs.length - 1 ? 'inline-block' : 'none';
|
||||
|
||||
stepIndicator.textContent = `Step ${currentTab + 1} of ${tabs.length}`;
|
||||
const progress = ((currentTab + 1) / tabs.length) * 100;
|
||||
progressBar.style.width = progress + '%';
|
||||
progressBar.setAttribute('aria-valuenow', progress);
|
||||
}
|
||||
|
||||
// Navigate to specific tab
|
||||
function goToTab(index) {
|
||||
if (index < 0 || index >= tabs.length) return;
|
||||
|
||||
// Validate current tab before moving forward
|
||||
if (index > currentTab && !validateCurrentTab()) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentTab = index;
|
||||
const tabId = tabs[index];
|
||||
const tabButton = document.getElementById(tabId + '-tab');
|
||||
|
||||
if (tabButton) {
|
||||
const tab = new bootstrap.Tab(tabButton);
|
||||
tab.show();
|
||||
}
|
||||
|
||||
updateButtons();
|
||||
saveDraft();
|
||||
}
|
||||
|
||||
// ISSUE 5 FIX: Validate current tab with single toast
|
||||
function validateCurrentTab() {
|
||||
const currentTabPane = document.getElementById(tabs[currentTab]);
|
||||
if (!currentTabPane) return true;
|
||||
|
||||
const inputs = currentTabPane.querySelectorAll('input[required], select[required], textarea[required]');
|
||||
let isValid = true;
|
||||
let errorCount = 0;
|
||||
|
||||
inputs.forEach(input => {
|
||||
// Skip file inputs for validation
|
||||
if (input.type === 'file') return;
|
||||
|
||||
if (!input.value || (input.type === 'email' && !isValidEmail(input.value))) {
|
||||
input.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
errorCount++;
|
||||
|
||||
// Show inline error message
|
||||
let errorDiv = input.nextElementSibling;
|
||||
if (!errorDiv || !errorDiv.classList.contains('invalid-feedback')) {
|
||||
errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'invalid-feedback';
|
||||
input.parentNode.insertBefore(errorDiv, input.nextSibling);
|
||||
}
|
||||
errorDiv.textContent = input.dataset.errorMessage || 'This field is required.';
|
||||
errorDiv.style.display = 'block';
|
||||
} else {
|
||||
input.classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
// ISSUE 5 FIX: Show only ONE toast per tab validation
|
||||
if (!isValid && !toastShown[currentTab]) {
|
||||
showToast(`Please fill in all required fields (${errorCount} field${errorCount > 1 ? 's' : ''} missing)`, 'error');
|
||||
toastShown[currentTab] = true;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
// Email validation
|
||||
function isValidEmail(email) {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
}
|
||||
|
||||
// Attach event listeners
|
||||
function attachEventListeners() {
|
||||
prevBtn.addEventListener('click', () => goToTab(currentTab - 1));
|
||||
nextBtn.addEventListener('click', () => goToTab(currentTab + 1));
|
||||
submitBtn.addEventListener('click', handleSubmit);
|
||||
|
||||
// Tab click navigation
|
||||
document.querySelectorAll('#clubModalTabs button[data-bs-toggle="tab"]').forEach((button, index) => {
|
||||
button.addEventListener('click', (e) => {
|
||||
// Reset toast tracking for the new tab
|
||||
toastShown[index] = false;
|
||||
|
||||
// Allow clicking on previous tabs, but validate before going forward
|
||||
if (index > currentTab && !validateCurrentTab()) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
currentTab = index;
|
||||
updateButtons();
|
||||
});
|
||||
});
|
||||
|
||||
// Clear validation on input
|
||||
form.addEventListener('input', (e) => {
|
||||
if (e.target.classList.contains('is-invalid')) {
|
||||
e.target.classList.remove('is-invalid');
|
||||
// Reset toast for this tab
|
||||
toastShown[currentTab] = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Save draft periodically
|
||||
setInterval(saveDraft, 30000); // Every 30 seconds
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
async function handleSubmit() {
|
||||
// Validate all tabs
|
||||
let allValid = true;
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
currentTab = i;
|
||||
if (!validateCurrentTab()) {
|
||||
allValid = false;
|
||||
goToTab(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!allValid) return;
|
||||
|
||||
const formData = new FormData(form);
|
||||
const mode = form.dataset.mode;
|
||||
const clubId = form.dataset.clubId;
|
||||
|
||||
// Show loading state
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Saving...';
|
||||
|
||||
try {
|
||||
const url = mode === 'edit'
|
||||
? `/admin/clubs/${clubId}`
|
||||
: '/admin/clubs';
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showToast(data.message || 'Club saved successfully!', 'success');
|
||||
clearDraft();
|
||||
|
||||
// Close modal and reload page
|
||||
setTimeout(() => {
|
||||
bootstrap.Modal.getInstance(modal).hide();
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
showToast(data.message || 'An error occurred', 'error');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="bi bi-check-circle me-2"></i>' + (mode === 'edit' ? 'Update Club' : 'Create Club');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('An error occurred while saving', 'error');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="bi bi-check-circle me-2"></i>' + (mode === 'edit' ? 'Update Club' : 'Create Club');
|
||||
}
|
||||
}
|
||||
|
||||
// Save draft to localStorage
|
||||
function saveDraft() {
|
||||
if (form.dataset.mode === 'create') {
|
||||
const formData = new FormData(form);
|
||||
const draft = {};
|
||||
for (let [key, value] of formData.entries()) {
|
||||
// ISSUE 2 FIX: Skip file inputs
|
||||
const input = form.querySelector(`[name="${key}"]`);
|
||||
if (input && input.type !== 'file') {
|
||||
draft[key] = value;
|
||||
}
|
||||
}
|
||||
localStorage.setItem('clubModalDraft', JSON.stringify(draft));
|
||||
}
|
||||
}
|
||||
|
||||
// ISSUE 2 FIX: Load draft from localStorage (skip file inputs)
|
||||
function loadDraft() {
|
||||
if (form.dataset.mode === 'create') {
|
||||
const draft = localStorage.getItem('clubModalDraft');
|
||||
if (draft) {
|
||||
try {
|
||||
const data = JSON.parse(draft);
|
||||
Object.keys(data).forEach(key => {
|
||||
const input = form.querySelector(`[name="${key}"]`);
|
||||
// ISSUE 2 FIX: Never set value on file inputs
|
||||
if (input && input.type !== 'file' && !input.value) {
|
||||
input.value = data[key];
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error loading draft:', e);
|
||||
// Don't show toast for draft loading errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear draft
|
||||
function clearDraft() {
|
||||
localStorage.removeItem('clubModalDraft');
|
||||
}
|
||||
|
||||
// Show toast notification
|
||||
function showToast(message, type = 'info') {
|
||||
// Use your existing toast notification system
|
||||
if (typeof Toast !== 'undefined') {
|
||||
if (type === 'success') {
|
||||
Toast.success('Success', message);
|
||||
} else if (type === 'error') {
|
||||
Toast.error('Error', message);
|
||||
} else {
|
||||
Toast.info('Info', message);
|
||||
}
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when modal is shown
|
||||
modal.addEventListener('shown.bs.modal', init);
|
||||
|
||||
// Reset on modal close
|
||||
modal.addEventListener('hidden.bs.modal', () => {
|
||||
currentTab = 0;
|
||||
draftLoaded = false;
|
||||
toastShown = {};
|
||||
form.reset();
|
||||
updateButtons();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
@@ -0,0 +1,472 @@
|
||||
@props(['mode' => 'create', 'club' => null])
|
||||
|
||||
@php
|
||||
$isEdit = $mode === 'edit' && $club;
|
||||
$modalId = 'clubModal';
|
||||
$modalTitle = $isEdit ? 'Edit Club' : 'Create New Club';
|
||||
@endphp
|
||||
|
||||
<!-- Club Modal -->
|
||||
<div class="modal fade" id="{{ $modalId }}" tabindex="-1" aria-labelledby="{{ $modalId }}Label" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||||
<div class="modal-dialog modal-dialog-centered modal-xl">
|
||||
<div class="modal-content" style="border-radius: 1rem; border: none; max-height: 90vh; display: flex; flex-direction: column;">
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header border-0 pb-0" style="padding: 1.5rem 1.5rem 0;">
|
||||
<div class="w-100">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<div>
|
||||
<h4 class="modal-title fw-bold mb-1" id="{{ $modalId }}Label">{{ $modalTitle }}</h4>
|
||||
<p class="text-muted small mb-0">Fill in the information across all tabs</p>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<!-- Progress Indicator -->
|
||||
<div class="d-flex align-items-center gap-2 mb-3">
|
||||
<span class="badge bg-primary" id="stepIndicator">Step 1 of 5</span>
|
||||
<div class="progress flex-grow-1" style="height: 6px;">
|
||||
<div class="progress-bar" id="progressBar" role="progressbar" style="width: 20%;" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<ul class="nav nav-tabs border-0" id="clubModalTabs" role="tablist" style="gap: 0.5rem; flex-wrap: nowrap; overflow-x: auto;">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="basic-tab" data-bs-toggle="tab" data-bs-target="#basic" type="button" role="tab" aria-controls="basic" aria-selected="true" data-step="1">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
<span class="d-none d-md-inline">Basic Info</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="identity-tab" data-bs-toggle="tab" data-bs-target="#identity" type="button" role="tab" aria-controls="identity" aria-selected="false" data-step="2">
|
||||
<i class="bi bi-palette me-2"></i>
|
||||
<span class="d-none d-md-inline">Identity & Branding</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="location-tab" data-bs-toggle="tab" data-bs-target="#location" type="button" role="tab" aria-controls="location" aria-selected="false" data-step="3">
|
||||
<i class="bi bi-geo-alt me-2"></i>
|
||||
<span class="d-none d-md-inline">Location</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="contact-tab" data-bs-toggle="tab" data-bs-target="#contact" type="button" role="tab" aria-controls="contact" aria-selected="false" data-step="4">
|
||||
<i class="bi bi-telephone me-2"></i>
|
||||
<span class="d-none d-md-inline">Contact</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="finance-tab" data-bs-toggle="tab" data-bs-target="#finance" type="button" role="tab" aria-controls="finance" aria-selected="false" data-step="5">
|
||||
<i class="bi bi-bank me-2"></i>
|
||||
<span class="d-none d-md-inline">Finance & Settings</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body (Scrollable) -->
|
||||
<div class="modal-body" style="padding: 1.5rem; overflow-y: auto; flex: 1;">
|
||||
<form id="clubForm" data-mode="{{ $mode }}" data-club-id="{{ $club->id ?? '' }}">
|
||||
@csrf
|
||||
@if($isEdit)
|
||||
@method('PUT')
|
||||
@endif
|
||||
|
||||
<div class="tab-content" id="clubModalTabContent">
|
||||
<!-- Tab 1: Basic Information -->
|
||||
<div class="tab-pane fade show active" id="basic" role="tabpanel" aria-labelledby="basic-tab">
|
||||
<x-club-modal.tabs.basic-info :club="$club" :mode="$mode" />
|
||||
</div>
|
||||
|
||||
<!-- Tab 2: Identity & Branding -->
|
||||
<div class="tab-pane fade" id="identity" role="tabpanel" aria-labelledby="identity-tab">
|
||||
<x-club-modal.tabs.identity-branding :club="$club" :mode="$mode" />
|
||||
</div>
|
||||
|
||||
<!-- Tab 3: Location -->
|
||||
<div class="tab-pane fade" id="location" role="tabpanel" aria-labelledby="location-tab">
|
||||
<x-club-modal.tabs.location :club="$club" :mode="$mode" />
|
||||
</div>
|
||||
|
||||
<!-- Tab 4: Contact -->
|
||||
<div class="tab-pane fade" id="contact" role="tabpanel" aria-labelledby="contact-tab">
|
||||
<x-club-modal.tabs.contact :club="$club" :mode="$mode" />
|
||||
</div>
|
||||
|
||||
<!-- Tab 5: Finance & Settings -->
|
||||
<div class="tab-pane fade" id="finance" role="tabpanel" aria-labelledby="finance-tab">
|
||||
<x-club-modal.tabs.finance-settings :club="$club" :mode="$mode" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<div class="modal-footer border-0" style="padding: 1rem 1.5rem 1.5rem; gap: 0.75rem;">
|
||||
<button type="button" class="btn btn-secondary" id="prevBtn" style="display: none;">
|
||||
<i class="bi bi-arrow-left me-2"></i>Back
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="nextBtn">
|
||||
Next<i class="bi bi-arrow-right ms-2"></i>
|
||||
</button>
|
||||
<button type="button" class="btn text-white" id="submitBtn" style="display: none; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||||
<i class="bi bi-check-circle me-2"></i>{{ $isEdit ? 'Update Club' : 'Create Club' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
/* Club Modal Custom Styles */
|
||||
#clubModal .nav-tabs {
|
||||
border-bottom: 2px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
#clubModal .nav-tabs .nav-link {
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-weight: 500;
|
||||
padding: 0.75rem 1rem;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#clubModal .nav-tabs .nav-link:hover {
|
||||
color: hsl(var(--primary));
|
||||
border-bottom-color: hsl(var(--primary) / 0.3);
|
||||
}
|
||||
|
||||
#clubModal .nav-tabs .nav-link.active {
|
||||
color: hsl(var(--primary));
|
||||
border-bottom-color: hsl(var(--primary));
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#clubModal .nav-tabs .nav-link i {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
#clubModal .modal-body {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: hsl(var(--border)) transparent;
|
||||
}
|
||||
|
||||
#clubModal .modal-body::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
#clubModal .modal-body::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#clubModal .modal-body::-webkit-scrollbar-thumb {
|
||||
background-color: hsl(var(--border));
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#clubModal .form-label {
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#clubModal .form-control:focus,
|
||||
#clubModal .form-select:focus {
|
||||
border-color: hsl(var(--primary));
|
||||
box-shadow: 0 0 0 0.2rem hsl(var(--primary) / 0.15);
|
||||
}
|
||||
|
||||
#clubModal .tab-pane {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
#clubModal .modal-xl {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
#clubModal .nav-tabs .nav-link span {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#clubModal .nav-tabs .nav-link {
|
||||
padding: 0.75rem 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/qrcode.min.js"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
|
||||
<script>
|
||||
// Club Modal Controller
|
||||
(function() {
|
||||
const modal = document.getElementById('clubModal');
|
||||
if (!modal) return;
|
||||
|
||||
const form = document.getElementById('clubForm');
|
||||
const tabs = ['basic', 'identity', 'location', 'contact', 'finance'];
|
||||
let currentTab = 0;
|
||||
|
||||
const prevBtn = document.getElementById('prevBtn');
|
||||
const nextBtn = document.getElementById('nextBtn');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const stepIndicator = document.getElementById('stepIndicator');
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
|
||||
// Initialize
|
||||
function init() {
|
||||
updateButtons();
|
||||
attachEventListeners();
|
||||
loadDraft();
|
||||
}
|
||||
|
||||
// Update button visibility and progress
|
||||
function updateButtons() {
|
||||
prevBtn.style.display = currentTab === 0 ? 'none' : 'inline-block';
|
||||
nextBtn.style.display = currentTab === tabs.length - 1 ? 'none' : 'inline-block';
|
||||
submitBtn.style.display = currentTab === tabs.length - 1 ? 'inline-block' : 'none';
|
||||
|
||||
stepIndicator.textContent = `Step ${currentTab + 1} of ${tabs.length}`;
|
||||
const progress = ((currentTab + 1) / tabs.length) * 100;
|
||||
progressBar.style.width = progress + '%';
|
||||
progressBar.setAttribute('aria-valuenow', progress);
|
||||
}
|
||||
|
||||
// Navigate to specific tab
|
||||
function goToTab(index) {
|
||||
if (index < 0 || index >= tabs.length) return;
|
||||
|
||||
// Validate current tab before moving forward
|
||||
if (index > currentTab && !validateCurrentTab()) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentTab = index;
|
||||
const tabId = tabs[index];
|
||||
const tabButton = document.getElementById(tabId + '-tab');
|
||||
|
||||
if (tabButton) {
|
||||
const tab = new bootstrap.Tab(tabButton);
|
||||
tab.show();
|
||||
}
|
||||
|
||||
updateButtons();
|
||||
saveDraft();
|
||||
}
|
||||
|
||||
// Validate current tab
|
||||
function validateCurrentTab() {
|
||||
const currentTabPane = document.getElementById(tabs[currentTab]);
|
||||
if (!currentTabPane) return true;
|
||||
|
||||
const inputs = currentTabPane.querySelectorAll('input[required], select[required], textarea[required]');
|
||||
let isValid = true;
|
||||
|
||||
inputs.forEach(input => {
|
||||
if (!input.value || (input.type === 'email' && !isValidEmail(input.value))) {
|
||||
input.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
|
||||
// Show error message
|
||||
let errorDiv = input.nextElementSibling;
|
||||
if (!errorDiv || !errorDiv.classList.contains('invalid-feedback')) {
|
||||
errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'invalid-feedback';
|
||||
input.parentNode.insertBefore(errorDiv, input.nextSibling);
|
||||
}
|
||||
errorDiv.textContent = input.dataset.errorMessage || 'This field is required.';
|
||||
} else {
|
||||
input.classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
showToast('Please fill in all required fields', 'error');
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
// Email validation
|
||||
function isValidEmail(email) {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
}
|
||||
|
||||
// Attach event listeners
|
||||
function attachEventListeners() {
|
||||
prevBtn.addEventListener('click', () => goToTab(currentTab - 1));
|
||||
nextBtn.addEventListener('click', () => goToTab(currentTab + 1));
|
||||
submitBtn.addEventListener('click', handleSubmit);
|
||||
|
||||
// Tab click navigation
|
||||
document.querySelectorAll('#clubModalTabs button[data-bs-toggle="tab"]').forEach((button, index) => {
|
||||
button.addEventListener('click', (e) => {
|
||||
// Allow clicking on previous tabs, but validate before going forward
|
||||
if (index > currentTab && !validateCurrentTab()) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
currentTab = index;
|
||||
updateButtons();
|
||||
});
|
||||
});
|
||||
|
||||
// Clear validation on input
|
||||
form.addEventListener('input', (e) => {
|
||||
if (e.target.classList.contains('is-invalid')) {
|
||||
e.target.classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
// Save draft periodically
|
||||
setInterval(saveDraft, 30000); // Every 30 seconds
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
async function handleSubmit() {
|
||||
// Validate all tabs
|
||||
let allValid = true;
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
currentTab = i;
|
||||
if (!validateCurrentTab()) {
|
||||
allValid = false;
|
||||
goToTab(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!allValid) return;
|
||||
|
||||
const formData = new FormData(form);
|
||||
const mode = form.dataset.mode;
|
||||
const clubId = form.dataset.clubId;
|
||||
|
||||
// Show loading state
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Saving...';
|
||||
|
||||
try {
|
||||
const url = mode === 'edit'
|
||||
? `/admin/clubs/${clubId}`
|
||||
: '/admin/clubs';
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showToast(data.message || 'Club saved successfully!', 'success');
|
||||
clearDraft();
|
||||
|
||||
// Close modal and reload page
|
||||
setTimeout(() => {
|
||||
bootstrap.Modal.getInstance(modal).hide();
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
showToast(data.message || 'An error occurred', 'error');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="bi bi-check-circle me-2"></i>' + (mode === 'edit' ? 'Update Club' : 'Create Club');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('An error occurred while saving', 'error');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="bi bi-check-circle me-2"></i>' + (mode === 'edit' ? 'Update Club' : 'Create Club');
|
||||
}
|
||||
}
|
||||
|
||||
// Save draft to localStorage
|
||||
function saveDraft() {
|
||||
if (form.dataset.mode === 'create') {
|
||||
const formData = new FormData(form);
|
||||
const draft = {};
|
||||
for (let [key, value] of formData.entries()) {
|
||||
draft[key] = value;
|
||||
}
|
||||
localStorage.setItem('clubModalDraft', JSON.stringify(draft));
|
||||
}
|
||||
}
|
||||
|
||||
// Load draft from localStorage
|
||||
function loadDraft() {
|
||||
if (form.dataset.mode === 'create') {
|
||||
const draft = localStorage.getItem('clubModalDraft');
|
||||
if (draft) {
|
||||
try {
|
||||
const data = JSON.parse(draft);
|
||||
Object.keys(data).forEach(key => {
|
||||
const input = form.querySelector(`[name="${key}"]`);
|
||||
if (input && !input.value) {
|
||||
input.value = data[key];
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error loading draft:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear draft
|
||||
function clearDraft() {
|
||||
localStorage.removeItem('clubModalDraft');
|
||||
}
|
||||
|
||||
// Show toast notification
|
||||
function showToast(message, type = 'info') {
|
||||
// Use your existing toast notification system
|
||||
if (typeof Toast !== 'undefined') {
|
||||
if (type === 'success') {
|
||||
Toast.success('Success', message);
|
||||
} else if (type === 'error') {
|
||||
Toast.error('Error', message);
|
||||
} else {
|
||||
Toast.info('Info', message);
|
||||
}
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when modal is shown
|
||||
modal.addEventListener('shown.bs.modal', init);
|
||||
|
||||
// Reset on modal close
|
||||
modal.addEventListener('hidden.bs.modal', () => {
|
||||
currentTab = 0;
|
||||
form.reset();
|
||||
updateButtons();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
@@ -0,0 +1,719 @@
|
||||
@props(['mode' => 'create', 'club' => null])
|
||||
|
||||
@php
|
||||
$isEdit = $mode === 'edit' && $club;
|
||||
$modalId = 'clubModal';
|
||||
$modalTitle = $isEdit ? 'Edit Club' : 'Create New Club';
|
||||
@endphp
|
||||
|
||||
<!-- Club Modal -->
|
||||
<div class="modal fade" id="{{ $modalId }}" tabindex="-1" aria-labelledby="{{ $modalId }}Label" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||||
<div class="modal-dialog modal-dialog-centered modal-xl">
|
||||
<div class="modal-content" style="border-radius: 1rem; border: none; max-height: 90vh; display: flex; flex-direction: column;">
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header border-0 pb-0" style="padding: 1.5rem 1.5rem 0;">
|
||||
<div class="w-100">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<div>
|
||||
<h4 class="modal-title fw-bold mb-1" id="{{ $modalId }}Label">{{ $modalTitle }}</h4>
|
||||
<p class="text-muted small mb-0">Fill in the information across all tabs</p>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<!-- Progress Indicator -->
|
||||
<div class="d-flex align-items-center gap-2 mb-3">
|
||||
<span class="badge bg-primary" id="stepIndicator">Step 1 of 5</span>
|
||||
<div class="progress flex-grow-1" style="height: 6px;">
|
||||
<div class="progress-bar" id="progressBar" role="progressbar" style="width: 20%;" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<ul class="nav nav-tabs border-0" id="clubModalTabs" role="tablist" style="gap: 0.5rem; flex-wrap: nowrap; overflow-x: auto;">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="basic-tab" data-bs-toggle="tab" data-bs-target="#basic" type="button" role="tab" aria-controls="basic" aria-selected="true" data-step="1">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
<span class="d-none d-md-inline">Basic Info</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="identity-tab" data-bs-toggle="tab" data-bs-target="#identity" type="button" role="tab" aria-controls="identity" aria-selected="false" data-step="2">
|
||||
<i class="bi bi-palette me-2"></i>
|
||||
<span class="d-none d-md-inline">Identity & Branding</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="location-tab" data-bs-toggle="tab" data-bs-target="#location" type="button" role="tab" aria-controls="location" aria-selected="false" data-step="3">
|
||||
<i class="bi bi-geo-alt me-2"></i>
|
||||
<span class="d-none d-md-inline">Location</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="contact-tab" data-bs-toggle="tab" data-bs-target="#contact" type="button" role="tab" aria-controls="contact" aria-selected="false" data-step="4">
|
||||
<i class="bi bi-telephone me-2"></i>
|
||||
<span class="d-none d-md-inline">Contact</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="finance-tab" data-bs-toggle="tab" data-bs-target="#finance" type="button" role="tab" aria-controls="finance" aria-selected="false" data-step="5">
|
||||
<i class="bi bi-bank me-2"></i>
|
||||
<span class="d-none d-md-inline">Finance & Settings</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body (Scrollable) -->
|
||||
<div class="modal-body" style="padding: 1.5rem; overflow-y: auto; flex: 1;">
|
||||
<form id="clubForm" data-mode="{{ $mode }}" data-club-id="{{ $club->id ?? '' }}">
|
||||
@csrf
|
||||
@if($isEdit)
|
||||
@method('PUT')
|
||||
@endif
|
||||
|
||||
<div class="tab-content" id="clubModalTabContent">
|
||||
<!-- Tab 1: Basic Information -->
|
||||
<div class="tab-pane fade show active" id="basic" role="tabpanel" aria-labelledby="basic-tab">
|
||||
<x-club-modal.tabs.basic-info :club="$club" :mode="$mode" />
|
||||
</div>
|
||||
|
||||
<!-- Tab 2: Identity & Branding -->
|
||||
<div class="tab-pane fade" id="identity" role="tabpanel" aria-labelledby="identity-tab">
|
||||
<x-club-modal.tabs.identity-branding :club="$club" :mode="$mode" />
|
||||
</div>
|
||||
|
||||
<!-- Tab 3: Location -->
|
||||
<div class="tab-pane fade" id="location" role="tabpanel" aria-labelledby="location-tab">
|
||||
<x-club-modal.tabs.location :club="$club" :mode="$mode" />
|
||||
</div>
|
||||
|
||||
<!-- Tab 4: Contact -->
|
||||
<div class="tab-pane fade" id="contact" role="tabpanel" aria-labelledby="contact-tab">
|
||||
<x-club-modal.tabs.contact :club="$club" :mode="$mode" />
|
||||
</div>
|
||||
|
||||
<!-- Tab 5: Finance & Settings -->
|
||||
<div class="tab-pane fade" id="finance" role="tabpanel" aria-labelledby="finance-tab">
|
||||
<x-club-modal.tabs.finance-settings :club="$club" :mode="$mode" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<div class="modal-footer border-0" style="padding: 1rem 1.5rem 1.5rem; gap: 0.75rem;">
|
||||
<button type="button" class="btn btn-secondary" id="prevBtn" style="display: none;">
|
||||
<i class="bi bi-arrow-left me-2"></i>Back
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="nextBtn">
|
||||
Next<i class="bi bi-arrow-right ms-2"></i>
|
||||
</button>
|
||||
<button type="button" class="btn text-white" id="submitBtn" style="display: none; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||||
<i class="bi bi-check-circle me-2"></i>{{ $isEdit ? 'Update Club' : 'Create Club' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
/* Club Modal Custom Styles */
|
||||
|
||||
/* PART 3 FIX: Prevent vertical scrollbar in tabs header */
|
||||
#clubModal .modal-header {
|
||||
overflow-y: visible;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#clubModal .nav-tabs {
|
||||
border-bottom: 2px solid hsl(var(--border));
|
||||
overflow-y: hidden; /* PART 2 FIX: No vertical scroll */
|
||||
overflow-x: auto; /* Allow horizontal scroll for many tabs */
|
||||
flex-wrap: nowrap;
|
||||
scrollbar-width: none; /* PART 2 FIX: Hide scrollbar in Firefox */
|
||||
-ms-overflow-style: none; /* PART 2 FIX: Hide scrollbar in IE/Edge */
|
||||
}
|
||||
|
||||
/* PART 2 FIX: Hide scrollbar in Chrome/Safari */
|
||||
#clubModal .nav-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#clubModal .nav-tabs .nav-link {
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-weight: 500;
|
||||
padding: 0.75rem 1rem;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0; /* Prevent tabs from shrinking */
|
||||
}
|
||||
|
||||
#clubModal .nav-tabs .nav-link:hover {
|
||||
color: hsl(var(--primary));
|
||||
border-bottom-color: hsl(var(--primary) / 0.3);
|
||||
}
|
||||
|
||||
#clubModal .nav-tabs .nav-link.active {
|
||||
color: hsl(var(--primary));
|
||||
border-bottom-color: hsl(var(--primary));
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#clubModal .nav-tabs .nav-link i {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* PART 3 FIX: Only modal body should have vertical scroll */
|
||||
#clubModal .modal-body {
|
||||
overflow-y: auto; /* Vertical scroll for content */
|
||||
overflow-x: hidden; /* No horizontal scroll */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: hsl(var(--border)) transparent;
|
||||
}
|
||||
|
||||
#clubModal .modal-body::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
#clubModal .modal-body::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#clubModal .modal-body::-webkit-scrollbar-thumb {
|
||||
background-color: hsl(var(--border));
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#clubModal .form-label {
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#clubModal .form-control:focus,
|
||||
#clubModal .form-select:focus {
|
||||
border-color: hsl(var(--primary));
|
||||
box-shadow: 0 0 0 0.2rem hsl(var(--primary) / 0.15);
|
||||
}
|
||||
|
||||
#clubModal .tab-pane {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ISSUE 1 FIX: Internal User Picker Overlay Styles */
|
||||
.user-picker-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1060;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.user-picker-panel {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.user-picker-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.user-picker-body {
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-picker-item {
|
||||
padding: 1rem;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.user-picker-item:hover {
|
||||
border-color: hsl(var(--primary));
|
||||
background-color: hsl(var(--muted) / 0.3);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
#clubModal .modal-xl {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
#clubModal .nav-tabs .nav-link span {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#clubModal .nav-tabs .nav-link {
|
||||
padding: 0.75rem 0.5rem;
|
||||
}
|
||||
|
||||
.user-picker-panel {
|
||||
max-height: 90vh;
|
||||
}
|
||||
}
|
||||
|
||||
/* ISSUE 4 FIX: Map container styles */
|
||||
#clubMap {
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
border-radius: 0.5rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Hide Leaflet attribution */
|
||||
.leaflet-control-attribution {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/qrcode.min.js"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
|
||||
<script>
|
||||
// ISSUE 1 FIX: Internal User Picker Functions
|
||||
let allUsersData = [];
|
||||
|
||||
async function showUserPicker() {
|
||||
const overlay = document.getElementById('userPickerOverlay');
|
||||
if (overlay) {
|
||||
overlay.style.display = 'flex';
|
||||
// Load users
|
||||
await loadUsersInternal();
|
||||
// Focus on search input
|
||||
document.getElementById('userSearchInputInternal')?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function hideUserPicker() {
|
||||
const overlay = document.getElementById('userPickerOverlay');
|
||||
if (overlay) {
|
||||
overlay.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUsersInternal() {
|
||||
const loadingDiv = document.getElementById('userPickerLoadingInternal');
|
||||
const resultsDiv = document.getElementById('userPickerResultsInternal');
|
||||
const noResultsDiv = document.getElementById('userPickerNoResultsInternal');
|
||||
|
||||
if (loadingDiv) loadingDiv.style.display = 'block';
|
||||
if (resultsDiv) resultsDiv.innerHTML = '';
|
||||
if (noResultsDiv) noResultsDiv.style.display = 'none';
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/api/users');
|
||||
if (response.ok) {
|
||||
allUsersData = await response.json();
|
||||
displayUsersInternal(allUsersData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading users:', error);
|
||||
} finally {
|
||||
if (loadingDiv) loadingDiv.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function displayUsersInternal(users) {
|
||||
const resultsDiv = document.getElementById('userPickerResultsInternal');
|
||||
const noResultsDiv = document.getElementById('userPickerNoResultsInternal');
|
||||
|
||||
if (!resultsDiv) return;
|
||||
|
||||
if (users.length === 0) {
|
||||
resultsDiv.innerHTML = '';
|
||||
if (noResultsDiv) noResultsDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
if (noResultsDiv) noResultsDiv.style.display = 'none';
|
||||
|
||||
resultsDiv.innerHTML = users.map(user => `
|
||||
<div class="user-picker-item" onclick="selectUserInternal(${user.id}, '${user.full_name}', '${user.email}', '${user.mobile_formatted || ''}', '${user.profile_picture || ''}')">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
${user.profile_picture
|
||||
? `<img src="/storage/${user.profile_picture}" alt="${user.full_name}" class="rounded-circle" style="width: 50px; height: 50px; object-fit: cover;">`
|
||||
: `<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center" style="width: 50px; height: 50px; font-size: 1.25rem; font-weight: 600;">${user.full_name.charAt(0)}</div>`
|
||||
}
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-semibold">${user.full_name}</div>
|
||||
<div class="small text-muted">
|
||||
<i class="bi bi-envelope me-1"></i>${user.email}
|
||||
${user.mobile_formatted ? `<span class="ms-2"><i class="bi bi-phone me-1"></i>${user.mobile_formatted}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function selectUserInternal(id, name, email, mobile, picture) {
|
||||
// Set hidden input
|
||||
const ownerInput = document.getElementById('owner_user_id');
|
||||
if (ownerInput) {
|
||||
ownerInput.value = id;
|
||||
ownerInput.dispatchEvent(new Event('change'));
|
||||
}
|
||||
|
||||
// Update display
|
||||
const ownerDisplay = document.getElementById('ownerDisplay');
|
||||
if (ownerDisplay) {
|
||||
ownerDisplay.innerHTML = `
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
${picture
|
||||
? `<img src="/storage/${picture}" alt="${name}" class="rounded-circle" style="width: 50px; height: 50px; object-fit: cover;">`
|
||||
: `<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center" style="width: 50px; height: 50px; font-size: 1.25rem; font-weight: 600;">${name.charAt(0)}</div>`
|
||||
}
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-semibold">${name}</div>
|
||||
<div class="small text-muted">
|
||||
<i class="bi bi-envelope me-1"></i>${email}
|
||||
${mobile ? `<span class="ms-2"><i class="bi bi-phone me-1"></i>${mobile}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Hide picker
|
||||
hideUserPicker();
|
||||
}
|
||||
|
||||
// Search functionality for internal user picker
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const searchInput = document.getElementById('userSearchInputInternal');
|
||||
if (searchInput) {
|
||||
let searchTimeout;
|
||||
searchInput.addEventListener('input', function() {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
const searchTerm = this.value.toLowerCase();
|
||||
const filtered = allUsersData.filter(user =>
|
||||
user.full_name.toLowerCase().includes(searchTerm) ||
|
||||
user.email.toLowerCase().includes(searchTerm) ||
|
||||
(user.mobile_formatted && user.mobile_formatted.includes(searchTerm))
|
||||
);
|
||||
displayUsersInternal(filtered);
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Club Modal Controller
|
||||
(function() {
|
||||
const modal = document.getElementById('clubModal');
|
||||
if (!modal) return;
|
||||
|
||||
const form = document.getElementById('clubForm');
|
||||
const tabs = ['basic', 'identity', 'location', 'contact', 'finance'];
|
||||
let currentTab = 0;
|
||||
let draftLoaded = false; // ISSUE 2 & 5 FIX: Track if draft was loaded
|
||||
let toastShown = {}; // ISSUE 5 FIX: Track shown toasts per tab
|
||||
|
||||
const prevBtn = document.getElementById('prevBtn');
|
||||
const nextBtn = document.getElementById('nextBtn');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const stepIndicator = document.getElementById('stepIndicator');
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
|
||||
// Initialize
|
||||
function init() {
|
||||
updateButtons();
|
||||
attachEventListeners();
|
||||
// ISSUE 2 & 5 FIX: Load draft only once on modal open
|
||||
if (!draftLoaded && form.dataset.mode === 'create') {
|
||||
loadDraft();
|
||||
draftLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update button visibility and progress
|
||||
function updateButtons() {
|
||||
prevBtn.style.display = currentTab === 0 ? 'none' : 'inline-block';
|
||||
nextBtn.style.display = currentTab === tabs.length - 1 ? 'none' : 'inline-block';
|
||||
submitBtn.style.display = currentTab === tabs.length - 1 ? 'inline-block' : 'none';
|
||||
|
||||
stepIndicator.textContent = `Step ${currentTab + 1} of ${tabs.length}`;
|
||||
const progress = ((currentTab + 1) / tabs.length) * 100;
|
||||
progressBar.style.width = progress + '%';
|
||||
progressBar.setAttribute('aria-valuenow', progress);
|
||||
}
|
||||
|
||||
// Navigate to specific tab
|
||||
function goToTab(index) {
|
||||
if (index < 0 || index >= tabs.length) return;
|
||||
|
||||
// Validate current tab before moving forward
|
||||
if (index > currentTab && !validateCurrentTab()) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentTab = index;
|
||||
const tabId = tabs[index];
|
||||
const tabButton = document.getElementById(tabId + '-tab');
|
||||
|
||||
if (tabButton) {
|
||||
const tab = new bootstrap.Tab(tabButton);
|
||||
tab.show();
|
||||
}
|
||||
|
||||
updateButtons();
|
||||
saveDraft();
|
||||
}
|
||||
|
||||
// ISSUE 5 FIX: Validate current tab with single toast
|
||||
function validateCurrentTab() {
|
||||
const currentTabPane = document.getElementById(tabs[currentTab]);
|
||||
if (!currentTabPane) return true;
|
||||
|
||||
const inputs = currentTabPane.querySelectorAll('input[required], select[required], textarea[required]');
|
||||
let isValid = true;
|
||||
let errorCount = 0;
|
||||
|
||||
inputs.forEach(input => {
|
||||
// Skip file inputs for validation
|
||||
if (input.type === 'file') return;
|
||||
|
||||
if (!input.value || (input.type === 'email' && !isValidEmail(input.value))) {
|
||||
input.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
errorCount++;
|
||||
|
||||
// Show inline error message
|
||||
let errorDiv = input.nextElementSibling;
|
||||
if (!errorDiv || !errorDiv.classList.contains('invalid-feedback')) {
|
||||
errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'invalid-feedback';
|
||||
input.parentNode.insertBefore(errorDiv, input.nextSibling);
|
||||
}
|
||||
errorDiv.textContent = input.dataset.errorMessage || 'This field is required.';
|
||||
errorDiv.style.display = 'block';
|
||||
} else {
|
||||
input.classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
// ISSUE 5 FIX: Show only ONE toast per tab validation
|
||||
if (!isValid && !toastShown[currentTab]) {
|
||||
showToast(`Please fill in all required fields (${errorCount} field${errorCount > 1 ? 's' : ''} missing)`, 'error');
|
||||
toastShown[currentTab] = true;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
// Email validation
|
||||
function isValidEmail(email) {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
}
|
||||
|
||||
// Attach event listeners
|
||||
function attachEventListeners() {
|
||||
prevBtn.addEventListener('click', () => goToTab(currentTab - 1));
|
||||
nextBtn.addEventListener('click', () => goToTab(currentTab + 1));
|
||||
submitBtn.addEventListener('click', handleSubmit);
|
||||
|
||||
// Tab click navigation
|
||||
document.querySelectorAll('#clubModalTabs button[data-bs-toggle="tab"]').forEach((button, index) => {
|
||||
button.addEventListener('click', (e) => {
|
||||
// Reset toast tracking for the new tab
|
||||
toastShown[index] = false;
|
||||
|
||||
// Allow clicking on previous tabs, but validate before going forward
|
||||
if (index > currentTab && !validateCurrentTab()) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
currentTab = index;
|
||||
updateButtons();
|
||||
});
|
||||
});
|
||||
|
||||
// Clear validation on input
|
||||
form.addEventListener('input', (e) => {
|
||||
if (e.target.classList.contains('is-invalid')) {
|
||||
e.target.classList.remove('is-invalid');
|
||||
// Reset toast for this tab
|
||||
toastShown[currentTab] = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Save draft periodically
|
||||
setInterval(saveDraft, 30000); // Every 30 seconds
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
async function handleSubmit() {
|
||||
// Validate all tabs
|
||||
let allValid = true;
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
currentTab = i;
|
||||
if (!validateCurrentTab()) {
|
||||
allValid = false;
|
||||
goToTab(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!allValid) return;
|
||||
|
||||
const formData = new FormData(form);
|
||||
const mode = form.dataset.mode;
|
||||
const clubId = form.dataset.clubId;
|
||||
|
||||
// Show loading state
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Saving...';
|
||||
|
||||
try {
|
||||
const url = mode === 'edit'
|
||||
? `/admin/clubs/${clubId}`
|
||||
: '/admin/clubs';
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showToast(data.message || 'Club saved successfully!', 'success');
|
||||
clearDraft();
|
||||
|
||||
// Close modal and reload page
|
||||
setTimeout(() => {
|
||||
bootstrap.Modal.getInstance(modal).hide();
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
showToast(data.message || 'An error occurred', 'error');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="bi bi-check-circle me-2"></i>' + (mode === 'edit' ? 'Update Club' : 'Create Club');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('An error occurred while saving', 'error');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="bi bi-check-circle me-2"></i>' + (mode === 'edit' ? 'Update Club' : 'Create Club');
|
||||
}
|
||||
}
|
||||
|
||||
// Save draft to localStorage
|
||||
function saveDraft() {
|
||||
if (form.dataset.mode === 'create') {
|
||||
const formData = new FormData(form);
|
||||
const draft = {};
|
||||
for (let [key, value] of formData.entries()) {
|
||||
// ISSUE 2 FIX: Skip file inputs
|
||||
const input = form.querySelector(`[name="${key}"]`);
|
||||
if (input && input.type !== 'file') {
|
||||
draft[key] = value;
|
||||
}
|
||||
}
|
||||
localStorage.setItem('clubModalDraft', JSON.stringify(draft));
|
||||
}
|
||||
}
|
||||
|
||||
// ISSUE 2 FIX: Load draft from localStorage (skip file inputs)
|
||||
function loadDraft() {
|
||||
if (form.dataset.mode === 'create') {
|
||||
const draft = localStorage.getItem('clubModalDraft');
|
||||
if (draft) {
|
||||
try {
|
||||
const data = JSON.parse(draft);
|
||||
Object.keys(data).forEach(key => {
|
||||
const input = form.querySelector(`[name="${key}"]`);
|
||||
// ISSUE 2 FIX: Never set value on file inputs
|
||||
if (input && input.type !== 'file' && !input.value) {
|
||||
input.value = data[key];
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error loading draft:', e);
|
||||
// Don't show toast for draft loading errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear draft
|
||||
function clearDraft() {
|
||||
localStorage.removeItem('clubModalDraft');
|
||||
}
|
||||
|
||||
// Show toast notification
|
||||
function showToast(message, type = 'info') {
|
||||
// Use your existing toast notification system
|
||||
if (typeof Toast !== 'undefined') {
|
||||
if (type === 'success') {
|
||||
Toast.success('Success', message);
|
||||
} else if (type === 'error') {
|
||||
Toast.error('Error', message);
|
||||
} else {
|
||||
Toast.info('Info', message);
|
||||
}
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when modal is shown
|
||||
modal.addEventListener('shown.bs.modal', init);
|
||||
|
||||
// Reset on modal close
|
||||
modal.addEventListener('hidden.bs.modal', () => {
|
||||
currentTab = 0;
|
||||
draftLoaded = false;
|
||||
toastShown = {};
|
||||
form.reset();
|
||||
updateButtons();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
@@ -0,0 +1,284 @@
|
||||
@props(['club' => null, 'mode' => 'create'])
|
||||
|
||||
@php
|
||||
$isEdit = $mode === 'edit' && $club;
|
||||
@endphp
|
||||
|
||||
<div class="container-fluid px-0">
|
||||
<h5 class="fw-bold mb-3">Basic Information</h5>
|
||||
<p class="text-muted mb-4">Core details about the club</p>
|
||||
|
||||
<!-- Club Name -->
|
||||
<div class="mb-4">
|
||||
<label for="club_name" class="form-label">
|
||||
Club Name <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="club_name"
|
||||
name="club_name"
|
||||
value="{{ $club->club_name ?? old('club_name') }}"
|
||||
required
|
||||
data-error-message="Club name is required"
|
||||
placeholder="e.g., Bahrain Taekwondo Academy">
|
||||
<div class="invalid-feedback">Club name is required.</div>
|
||||
</div>
|
||||
|
||||
<!-- Club Owner -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">
|
||||
Club Owner <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="hidden" id="owner_user_id" name="owner_user_id" value="{{ $club->owner_user_id ?? old('owner_user_id') }}" required>
|
||||
|
||||
<div id="ownerDisplay" class="border rounded p-3 mb-2" style="background-color: hsl(var(--muted) / 0.3);">
|
||||
@if($isEdit && $club->owner)
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
@if($club->owner->profile_picture)
|
||||
<img src="{{ asset('storage/' . $club->owner->profile_picture) }}"
|
||||
alt="{{ $club->owner->full_name }}"
|
||||
class="rounded-circle"
|
||||
style="width: 50px; height: 50px; object-fit: cover;">
|
||||
@else
|
||||
<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center"
|
||||
style="width: 50px; height: 50px; font-size: 1.25rem; font-weight: 600;">
|
||||
{{ substr($club->owner->full_name, 0, 1) }}
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-semibold">{{ $club->owner->full_name }}</div>
|
||||
<div class="small text-muted">
|
||||
<i class="bi bi-envelope me-1"></i>{{ $club->owner->email }}
|
||||
@if($club->owner->mobile)
|
||||
<span class="ms-2"><i class="bi bi-phone me-1"></i>{{ $club->owner->mobile_formatted }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center text-muted py-3" id="noOwnerSelected">
|
||||
<i class="bi bi-person-plus fs-3 mb-2"></i>
|
||||
<p class="mb-0">No owner selected</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="showUserPicker()">
|
||||
<i class="bi bi-search me-2"></i>Select Club Owner
|
||||
</button>
|
||||
<div class="invalid-feedback d-block" id="ownerError" style="display: none !important;">
|
||||
Please select a club owner.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Internal User Picker Overlay (NOT a separate modal) -->
|
||||
<div id="userPickerOverlay" class="user-picker-overlay" style="display: none;">
|
||||
<div class="user-picker-panel">
|
||||
<div class="user-picker-header">
|
||||
<div>
|
||||
<h5 class="fw-bold mb-1">Select Club Owner</h5>
|
||||
<p class="text-muted small mb-0">Search and select a user to be the club owner</p>
|
||||
</div>
|
||||
<button type="button" class="btn-close" onclick="hideUserPicker()"></button>
|
||||
</div>
|
||||
|
||||
<div class="user-picker-body">
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-white">
|
||||
<i class="bi bi-search"></i>
|
||||
</span>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="userSearchInputInternal"
|
||||
placeholder="Search by name, email, or phone..."
|
||||
autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="userPickerLoadingInternal" class="text-center py-5" style="display: none;">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="text-muted mt-2">Searching users...</p>
|
||||
</div>
|
||||
|
||||
<div id="userPickerResultsInternal" style="max-height: 400px; overflow-y: auto;"></div>
|
||||
|
||||
<div id="userPickerNoResultsInternal" class="text-center py-5" style="display: none;">
|
||||
<i class="bi bi-person-x fs-1 text-muted mb-3"></i>
|
||||
<p class="text-muted mb-0">No users found</p>
|
||||
<small class="text-muted">Try a different search term</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Established Date -->
|
||||
<div class="mb-4">
|
||||
<label for="established_date" class="form-label">
|
||||
Established Date
|
||||
</label>
|
||||
<input type="date"
|
||||
class="form-control"
|
||||
id="established_date"
|
||||
name="established_date"
|
||||
value="{{ $club->established_date ?? old('established_date') }}"
|
||||
max="{{ date('Y-m-d') }}">
|
||||
<small class="text-muted">When was the club founded?</small>
|
||||
</div>
|
||||
|
||||
<!-- Slogan -->
|
||||
<div class="mb-4">
|
||||
<label for="slogan" class="form-label">
|
||||
Slogan
|
||||
</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="slogan"
|
||||
name="slogan"
|
||||
value="{{ $club->slogan ?? old('slogan') }}"
|
||||
placeholder="e.g., Excellence in Martial Arts"
|
||||
maxlength="100">
|
||||
<small class="text-muted">A short, memorable tagline (max 100 characters)</small>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="mb-4">
|
||||
<label for="description" class="form-label">
|
||||
Description
|
||||
</label>
|
||||
<textarea class="form-control"
|
||||
id="description"
|
||||
name="description"
|
||||
rows="4"
|
||||
placeholder="Describe your club, its mission, and what makes it unique..."
|
||||
maxlength="1000">{{ $club->description ?? old('description') }}</textarea>
|
||||
<small class="text-muted">
|
||||
<span id="descriptionCount">0</span>/1000 characters
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Commercial Registration -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label for="commercial_reg_number" class="form-label">
|
||||
Commercial Registration Number
|
||||
</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="commercial_reg_number"
|
||||
name="commercial_reg_number"
|
||||
value="{{ $club->commercial_reg_number ?? old('commercial_reg_number') }}"
|
||||
placeholder="e.g., CR-123456">
|
||||
<small class="text-muted">Official business registration number</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="commercial_reg_file" class="form-label">
|
||||
Registration Document
|
||||
</label>
|
||||
<input type="file"
|
||||
class="form-control"
|
||||
id="commercial_reg_file"
|
||||
name="commercial_reg_file"
|
||||
accept=".pdf,.jpg,.jpeg,.png">
|
||||
<small class="text-muted">Upload registration certificate (optional)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VAT Information -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label for="vat_reg_number" class="form-label">
|
||||
VAT Registration Number
|
||||
</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="vat_reg_number"
|
||||
name="vat_reg_number"
|
||||
value="{{ $club->vat_reg_number ?? old('vat_reg_number') }}"
|
||||
placeholder="e.g., VAT-123456789">
|
||||
<small class="text-muted">Tax registration number (if applicable)</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="vat_percentage" class="form-label">
|
||||
VAT Percentage (%)
|
||||
</label>
|
||||
<input type="number"
|
||||
class="form-control"
|
||||
id="vat_percentage"
|
||||
name="vat_percentage"
|
||||
value="{{ $club->vat_percentage ?? old('vat_percentage', '0') }}"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.01"
|
||||
placeholder="e.g., 5.00">
|
||||
<small class="text-muted">Default VAT rate for invoices</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VAT Certificate Upload -->
|
||||
<div class="mb-4">
|
||||
<label for="vat_certificate_file" class="form-label">
|
||||
VAT Certificate
|
||||
</label>
|
||||
<input type="file"
|
||||
class="form-control"
|
||||
id="vat_certificate_file"
|
||||
name="vat_certificate_file"
|
||||
accept=".pdf,.jpg,.jpeg,.png">
|
||||
<small class="text-muted">Upload VAT registration certificate (optional)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Character counter for description
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const descriptionTextarea = document.getElementById('description');
|
||||
const descriptionCount = document.getElementById('descriptionCount');
|
||||
|
||||
if (descriptionTextarea && descriptionCount) {
|
||||
function updateCount() {
|
||||
descriptionCount.textContent = descriptionTextarea.value.length;
|
||||
}
|
||||
|
||||
descriptionTextarea.addEventListener('input', updateCount);
|
||||
updateCount(); // Initial count
|
||||
}
|
||||
|
||||
// Auto-generate slug from club name (will be used in Identity tab)
|
||||
const clubNameInput = document.getElementById('club_name');
|
||||
if (clubNameInput) {
|
||||
clubNameInput.addEventListener('input', function() {
|
||||
const slug = this.value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
|
||||
const slugInput = document.getElementById('slug');
|
||||
if (slugInput && !slugInput.dataset.manuallyEdited) {
|
||||
slugInput.value = slug;
|
||||
// Trigger slug change event for URL preview
|
||||
slugInput.dispatchEvent(new Event('input'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Validate owner selection
|
||||
const ownerInput = document.getElementById('owner_user_id');
|
||||
if (ownerInput) {
|
||||
ownerInput.addEventListener('change', function() {
|
||||
const ownerError = document.getElementById('ownerError');
|
||||
if (this.value) {
|
||||
ownerError.style.display = 'none';
|
||||
this.classList.remove('is-invalid');
|
||||
} else {
|
||||
ownerError.style.display = 'block';
|
||||
this.classList.add('is-invalid');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,227 @@
|
||||
@props(['club' => null, 'mode' => 'create'])
|
||||
|
||||
@php
|
||||
$isEdit = $mode === 'edit' && $club;
|
||||
@endphp
|
||||
|
||||
<div class="container-fluid px-0">
|
||||
<h5 class="fw-bold mb-3">Contact Information</h5>
|
||||
<p class="text-muted mb-4">Set up how members can reach your club</p>
|
||||
|
||||
<!-- Club Email -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Club Email</label>
|
||||
|
||||
<!-- Toggle: Use Owner Email or Custom -->
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input"
|
||||
type="radio"
|
||||
name="email_option"
|
||||
id="email_option_owner"
|
||||
value="owner"
|
||||
{{ (!$isEdit || !$club->email) ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="email_option_owner">
|
||||
Use Club Owner's Email
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input"
|
||||
type="radio"
|
||||
name="email_option"
|
||||
id="email_option_custom"
|
||||
value="custom"
|
||||
{{ ($isEdit && $club->email) ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="email_option_custom">
|
||||
Use Custom Email
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Owner Email Display (Read-only) -->
|
||||
<div id="ownerEmailDisplay" class="border rounded p-3 mb-3" style="background-color: hsl(var(--muted) / 0.2); display: {{ (!$isEdit || !$club->email) ? 'block' : 'none' }};">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-envelope text-muted"></i>
|
||||
<span id="ownerEmailText" class="text-muted">
|
||||
@if($isEdit && $club->owner)
|
||||
{{ $club->owner->email }}
|
||||
@else
|
||||
Select a club owner first
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Email Input -->
|
||||
<div id="customEmailInput" style="display: {{ ($isEdit && $club->email) ? 'block' : 'none' }};">
|
||||
<input type="email"
|
||||
class="form-control"
|
||||
id="email"
|
||||
name="email"
|
||||
value="{{ $club->email ?? old('email') }}"
|
||||
placeholder="[email protected]">
|
||||
<small class="text-muted">A dedicated email address for club communications</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Club Phone -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Club Phone Number</label>
|
||||
|
||||
<!-- Toggle: Use Owner Phone or Custom -->
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input"
|
||||
type="radio"
|
||||
name="phone_option"
|
||||
id="phone_option_owner"
|
||||
value="owner"
|
||||
{{ (!$isEdit || !$club->phone) ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="phone_option_owner">
|
||||
Use Club Owner's Phone
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input"
|
||||
type="radio"
|
||||
name="phone_option"
|
||||
id="phone_option_custom"
|
||||
value="custom"
|
||||
{{ ($isEdit && $club->phone) ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="phone_option_custom">
|
||||
Use Custom Phone Number
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Owner Phone Display (Read-only) -->
|
||||
<div id="ownerPhoneDisplay" class="border rounded p-3 mb-3" style="background-color: hsl(var(--muted) / 0.2); display: {{ (!$isEdit || !$club->phone) ? 'block' : 'none' }};">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-phone text-muted"></i>
|
||||
<span id="ownerPhoneText" class="text-muted">
|
||||
@if($isEdit && $club->owner && $club->owner->mobile)
|
||||
{{ $club->owner->mobile_formatted }}
|
||||
@else
|
||||
Select a club owner first
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Phone Input -->
|
||||
<div id="customPhoneInput" style="display: {{ ($isEdit && $club->phone) ? 'block' : 'none' }};">
|
||||
<x-country-code-dropdown
|
||||
name="phone_code"
|
||||
id="phone_code"
|
||||
:value="$isEdit && $club->phone ? ($club->phone['code'] ?? '+973') : old('phone_code', '+973')"
|
||||
:required="false"
|
||||
:error="null">
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="phone_number"
|
||||
id="phone_number"
|
||||
value="{{ $isEdit && $club->phone ? ($club->phone['number'] ?? '') : old('phone_number') }}"
|
||||
placeholder="12345678">
|
||||
</x-country-code-dropdown>
|
||||
<small class="text-muted">A dedicated phone number for club inquiries</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Contact Info (Optional) -->
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
<strong>Tip:</strong> You can add more contact methods (WhatsApp, social media, website) in the <strong>Identity & Branding</strong> tab under Social Media Links.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Email option toggle
|
||||
const emailOptionOwner = document.getElementById('email_option_owner');
|
||||
const emailOptionCustom = document.getElementById('email_option_custom');
|
||||
const ownerEmailDisplay = document.getElementById('ownerEmailDisplay');
|
||||
const customEmailInput = document.getElementById('customEmailInput');
|
||||
const emailInput = document.getElementById('email');
|
||||
|
||||
if (emailOptionOwner && emailOptionCustom) {
|
||||
emailOptionOwner.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
ownerEmailDisplay.style.display = 'block';
|
||||
customEmailInput.style.display = 'none';
|
||||
if (emailInput) emailInput.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
emailOptionCustom.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
ownerEmailDisplay.style.display = 'none';
|
||||
customEmailInput.style.display = 'block';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Phone option toggle
|
||||
const phoneOptionOwner = document.getElementById('phone_option_owner');
|
||||
const phoneOptionCustom = document.getElementById('phone_option_custom');
|
||||
const ownerPhoneDisplay = document.getElementById('ownerPhoneDisplay');
|
||||
const customPhoneInput = document.getElementById('customPhoneInput');
|
||||
const phoneNumberInput = document.getElementById('phone_number');
|
||||
|
||||
if (phoneOptionOwner && phoneOptionCustom) {
|
||||
phoneOptionOwner.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
ownerPhoneDisplay.style.display = 'block';
|
||||
customPhoneInput.style.display = 'none';
|
||||
if (phoneNumberInput) phoneNumberInput.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
phoneOptionCustom.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
ownerPhoneDisplay.style.display = 'none';
|
||||
customPhoneInput.style.display = 'block';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update owner email/phone display when owner is selected
|
||||
document.addEventListener('ownerSelected', function(e) {
|
||||
const owner = e.detail;
|
||||
|
||||
// Update email display
|
||||
const ownerEmailText = document.getElementById('ownerEmailText');
|
||||
if (ownerEmailText && owner.email) {
|
||||
ownerEmailText.textContent = owner.email;
|
||||
}
|
||||
|
||||
// Update phone display
|
||||
const ownerPhoneText = document.getElementById('ownerPhoneText');
|
||||
if (ownerPhoneText && owner.mobile) {
|
||||
ownerPhoneText.textContent = owner.mobile;
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for owner selection from user picker
|
||||
const ownerInput = document.getElementById('owner_user_id');
|
||||
if (ownerInput) {
|
||||
// Create a MutationObserver to watch for value changes
|
||||
const observer = new MutationObserver(function(mutations) {
|
||||
mutations.forEach(function(mutation) {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
|
||||
// Owner changed, update displays if using owner's contact info
|
||||
updateOwnerContactInfo();
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe(ownerInput, { attributes: true });
|
||||
|
||||
ownerInput.addEventListener('change', updateOwnerContactInfo);
|
||||
}
|
||||
});
|
||||
|
||||
function updateOwnerContactInfo() {
|
||||
// This function would ideally fetch the owner's details
|
||||
// For now, it's handled by the user picker modal
|
||||
// which updates the display directly
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,346 @@
|
||||
@props(['club' => null, 'mode' => 'create'])
|
||||
|
||||
@php
|
||||
$isEdit = $mode === 'edit' && $club;
|
||||
@endphp
|
||||
|
||||
<div class="container-fluid px-0">
|
||||
<h5 class="fw-bold mb-3">Finance & Settings</h5>
|
||||
<p class="text-muted mb-4">Configure bank accounts and club status</p>
|
||||
|
||||
<!-- Bank Accounts Section -->
|
||||
<div class="mb-5">
|
||||
<h6 class="fw-semibold mb-3">
|
||||
<i class="bi bi-bank me-2"></i>Bank Accounts
|
||||
</h6>
|
||||
<p class="text-muted small mb-3">Add one or more bank accounts for receiving payments</p>
|
||||
|
||||
<div id="bankAccountsContainer">
|
||||
@if($isEdit && $club->bankAccounts && $club->bankAccounts->count() > 0)
|
||||
@foreach($club->bankAccounts as $index => $account)
|
||||
<div class="bank-account-block border rounded p-4 mb-3" data-index="{{ $index }}" style="background-color: hsl(var(--muted) / 0.1);">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-semibold mb-0">Bank Account #{{ $index + 1 }}</h6>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeBankAccount(this)">
|
||||
<i class="bi bi-trash"></i> Remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Bank Name <span class="text-danger">*</span></label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="bank_accounts[{{ $index }}][bank_name]"
|
||||
value="{{ $account->bank_name }}"
|
||||
placeholder="e.g., National Bank of Bahrain"
|
||||
required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Account Name <span class="text-danger">*</span></label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="bank_accounts[{{ $index }}][account_name]"
|
||||
value="{{ $account->account_name }}"
|
||||
placeholder="e.g., Club Name Ltd."
|
||||
required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Account Number</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="bank_accounts[{{ $index }}][account_number]"
|
||||
value="{{ $account->account_number }}"
|
||||
placeholder="e.g., 1234567890">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">IBAN</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="bank_accounts[{{ $index }}][iban]"
|
||||
value="{{ $account->iban }}"
|
||||
placeholder="e.g., BH67BMAG00001299123456"
|
||||
pattern="[A-Z]{2}[0-9]{2}[A-Z0-9]+">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">SWIFT/BIC Code</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="bank_accounts[{{ $index }}][swift_code]"
|
||||
value="{{ $account->swift_code }}"
|
||||
placeholder="e.g., BMAGBHBM"
|
||||
pattern="[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">BenefitPay Account</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="bank_accounts[{{ $index }}][benefitpay_account]"
|
||||
value="{{ $account->benefitpay_account ?? '' }}"
|
||||
placeholder="Optional">
|
||||
<small class="text-muted">Local payment system account number</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-outline-primary" onclick="addBankAccount()">
|
||||
<i class="bi bi-plus-circle me-2"></i>Add Bank Account
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Club Status & Visibility -->
|
||||
<div class="mb-4">
|
||||
<h6 class="fw-semibold mb-3">
|
||||
<i class="bi bi-gear me-2"></i>Club Status & Visibility
|
||||
</h6>
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- Club Status -->
|
||||
<div class="col-md-6">
|
||||
<label for="club_status" class="form-label">Club Status</label>
|
||||
<select class="form-select" id="club_status" name="club_status">
|
||||
<option value="active" {{ ($club->status ?? 'active') === 'active' ? 'selected' : '' }}>
|
||||
Active
|
||||
</option>
|
||||
<option value="inactive" {{ ($club->status ?? '') === 'inactive' ? 'selected' : '' }}>
|
||||
Inactive
|
||||
</option>
|
||||
<option value="pending" {{ ($club->status ?? '') === 'pending' ? 'selected' : '' }}>
|
||||
Pending
|
||||
</option>
|
||||
</select>
|
||||
<small class="text-muted">Current operational status of the club</small>
|
||||
</div>
|
||||
|
||||
<!-- Public Profile -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Public Profile</label>
|
||||
<div class="form-check form-switch" style="padding-top: 0.5rem;">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
id="public_profile_enabled"
|
||||
name="public_profile_enabled"
|
||||
value="1"
|
||||
{{ ($club->public_profile_enabled ?? true) ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="public_profile_enabled">
|
||||
Enable public profile page
|
||||
</label>
|
||||
</div>
|
||||
<small class="text-muted">Allow public access to club URL and QR code</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Enrollment Fee (Optional) -->
|
||||
<div class="mb-4">
|
||||
<label for="enrollment_fee" class="form-label">Enrollment Fee</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">{{ $club->currency ?? 'BHD' }}</span>
|
||||
<input type="number"
|
||||
class="form-control"
|
||||
id="enrollment_fee"
|
||||
name="enrollment_fee"
|
||||
value="{{ $club->enrollment_fee ?? old('enrollment_fee', '0.00') }}"
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="0.00">
|
||||
</div>
|
||||
<small class="text-muted">One-time fee for new members (optional)</small>
|
||||
</div>
|
||||
|
||||
<!-- Summary Info Box -->
|
||||
<div class="alert alert-light border" role="alert">
|
||||
<h6 class="alert-heading fw-semibold">
|
||||
<i class="bi bi-info-circle me-2"></i>Summary
|
||||
</h6>
|
||||
<ul class="mb-0 small">
|
||||
<li><strong>Bank Accounts:</strong> <span id="bankAccountCount">{{ $isEdit && $club->bankAccounts ? $club->bankAccounts->count() : 0 }}</span> configured</li>
|
||||
<li><strong>Status:</strong> Club will be set as <span id="statusSummary" class="fw-semibold">Active</span></li>
|
||||
<li><strong>Public Access:</strong> <span id="publicAccessSummary" class="fw-semibold">Enabled</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Metadata Info (Read-only) -->
|
||||
@if($isEdit)
|
||||
<div class="border-top pt-4 mt-4">
|
||||
<h6 class="text-muted small text-uppercase mb-3">Metadata</h6>
|
||||
<div class="row g-3 small text-muted">
|
||||
<div class="col-md-6">
|
||||
<i class="bi bi-calendar-plus me-2"></i>
|
||||
<strong>Created:</strong> {{ $club->created_at->format('M d, Y') }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="bi bi-calendar-check me-2"></i>
|
||||
<strong>Last Updated:</strong> {{ $club->updated_at->format('M d, Y') }}
|
||||
</div>
|
||||
@if($club->owner)
|
||||
<div class="col-md-12">
|
||||
<i class="bi bi-person me-2"></i>
|
||||
<strong>Owner:</strong> {{ $club->owner->full_name }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
let bankAccountIndex = {{ $isEdit && $club->bankAccounts ? $club->bankAccounts->count() : 0 }};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateSummary();
|
||||
|
||||
// Update summary when status changes
|
||||
const statusSelect = document.getElementById('club_status');
|
||||
if (statusSelect) {
|
||||
statusSelect.addEventListener('change', updateSummary);
|
||||
}
|
||||
|
||||
// Update summary when public profile toggle changes
|
||||
const publicProfileToggle = document.getElementById('public_profile_enabled');
|
||||
if (publicProfileToggle) {
|
||||
publicProfileToggle.addEventListener('change', updateSummary);
|
||||
}
|
||||
});
|
||||
|
||||
// Add bank account block
|
||||
function addBankAccount() {
|
||||
const container = document.getElementById('bankAccountsContainer');
|
||||
if (!container) return;
|
||||
|
||||
const block = document.createElement('div');
|
||||
block.className = 'bank-account-block border rounded p-4 mb-3';
|
||||
block.style.backgroundColor = 'hsl(var(--muted) / 0.1)';
|
||||
block.dataset.index = bankAccountIndex;
|
||||
block.innerHTML = `
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-semibold mb-0">Bank Account #${bankAccountIndex + 1}</h6>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeBankAccount(this)">
|
||||
<i class="bi bi-trash"></i> Remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Bank Name <span class="text-danger">*</span></label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="bank_accounts[${bankAccountIndex}][bank_name]"
|
||||
placeholder="e.g., National Bank of Bahrain"
|
||||
required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Account Name <span class="text-danger">*</span></label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="bank_accounts[${bankAccountIndex}][account_name]"
|
||||
placeholder="e.g., Club Name Ltd."
|
||||
required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Account Number</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="bank_accounts[${bankAccountIndex}][account_number]"
|
||||
placeholder="e.g., 1234567890">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">IBAN</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="bank_accounts[${bankAccountIndex}][iban]"
|
||||
placeholder="e.g., BH67BMAG00001299123456"
|
||||
pattern="[A-Z]{2}[0-9]{2}[A-Z0-9]+">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">SWIFT/BIC Code</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="bank_accounts[${bankAccountIndex}][swift_code]"
|
||||
placeholder="e.g., BMAGBHBM"
|
||||
pattern="[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">BenefitPay Account</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="bank_accounts[${bankAccountIndex}][benefitpay_account]"
|
||||
placeholder="Optional">
|
||||
<small class="text-muted">Local payment system account number</small>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(block);
|
||||
bankAccountIndex++;
|
||||
updateSummary();
|
||||
|
||||
// Scroll to the new account
|
||||
block.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
|
||||
// Remove bank account block
|
||||
function removeBankAccount(button) {
|
||||
const block = button.closest('.bank-account-block');
|
||||
if (block) {
|
||||
if (confirm('Are you sure you want to remove this bank account?')) {
|
||||
block.remove();
|
||||
updateSummary();
|
||||
renumberBankAccounts();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Renumber bank accounts after removal
|
||||
function renumberBankAccounts() {
|
||||
const blocks = document.querySelectorAll('.bank-account-block');
|
||||
blocks.forEach((block, index) => {
|
||||
const heading = block.querySelector('h6');
|
||||
if (heading) {
|
||||
heading.textContent = `Bank Account #${index + 1}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update summary
|
||||
function updateSummary() {
|
||||
// Update bank account count
|
||||
const bankAccountCount = document.querySelectorAll('.bank-account-block').length;
|
||||
const countElement = document.getElementById('bankAccountCount');
|
||||
if (countElement) {
|
||||
countElement.textContent = bankAccountCount;
|
||||
}
|
||||
|
||||
// Update status summary
|
||||
const statusSelect = document.getElementById('club_status');
|
||||
const statusSummary = document.getElementById('statusSummary');
|
||||
if (statusSelect && statusSummary) {
|
||||
const statusText = statusSelect.options[statusSelect.selectedIndex].text;
|
||||
statusSummary.textContent = statusText;
|
||||
|
||||
// Update color based on status
|
||||
statusSummary.className = 'fw-semibold';
|
||||
if (statusSelect.value === 'active') {
|
||||
statusSummary.classList.add('text-success');
|
||||
} else if (statusSelect.value === 'inactive') {
|
||||
statusSummary.classList.add('text-danger');
|
||||
} else {
|
||||
statusSummary.classList.add('text-warning');
|
||||
}
|
||||
}
|
||||
|
||||
// Update public access summary
|
||||
const publicToggle = document.getElementById('public_profile_enabled');
|
||||
const publicSummary = document.getElementById('publicAccessSummary');
|
||||
if (publicToggle && publicSummary) {
|
||||
publicSummary.textContent = publicToggle.checked ? 'Enabled' : 'Disabled';
|
||||
publicSummary.className = publicToggle.checked ? 'fw-semibold text-success' : 'fw-semibold text-muted';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,743 @@
|
||||
@props(['club' => null, 'mode' => 'create'])
|
||||
|
||||
@php
|
||||
$isEdit = $mode === 'edit' && $club;
|
||||
@endphp
|
||||
|
||||
<div class="container-fluid px-0">
|
||||
<h5 class="fw-bold mb-3">Identity & Branding</h5>
|
||||
<p class="text-muted mb-4">Define your club's public identity, URL, and visual branding</p>
|
||||
|
||||
<!-- Club Slug -->
|
||||
<div class="mb-4">
|
||||
<label for="slug" class="form-label">
|
||||
Club Slug <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-white">
|
||||
<i class="bi bi-link-45deg"></i>
|
||||
</span>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="slug"
|
||||
name="slug"
|
||||
value="{{ $club->slug ?? old('slug') }}"
|
||||
required
|
||||
pattern="[a-z0-9-]+"
|
||||
data-error-message="Slug is required and must be URL-friendly"
|
||||
placeholder="e.g., bh-taekwondo">
|
||||
</div>
|
||||
<small class="text-muted">URL-friendly identifier (lowercase letters, numbers, and hyphens only)</small>
|
||||
<div class="invalid-feedback">Please enter a valid slug.</div>
|
||||
</div>
|
||||
|
||||
<!-- Club URL Preview -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Club Public URL</label>
|
||||
<div class="border rounded p-3" style="background-color: hsl(var(--muted) / 0.2);">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-globe text-primary"></i>
|
||||
<code id="clubUrlPreview" class="text-primary mb-0">{{ url('/club/') }}/{{ $club->slug ?? 'your-club-slug' }}</code>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-auto" onclick="copyClubUrl()">
|
||||
<i class="bi bi-clipboard"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted">This is the public URL where members can view your club</small>
|
||||
</div>
|
||||
|
||||
<!-- QR Code -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Club QR Code</label>
|
||||
<div class="border rounded p-4 text-center" style="background-color: hsl(var(--muted) / 0.1);">
|
||||
<div id="qrCodeContainer" class="d-inline-block mb-3"></div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="downloadQRCode()">
|
||||
<i class="bi bi-download me-2"></i>Download QR Code
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="printQRCode()">
|
||||
<i class="bi bi-printer me-2"></i>Print
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted">Share this QR code for easy access to your club's page</small>
|
||||
</div>
|
||||
|
||||
<!-- Logo and Cover Images -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label d-block">Club Logo <span class="text-danger">*</span></label>
|
||||
<div class="text-center">
|
||||
<!-- Logo Preview -->
|
||||
<div class="cropper-preview-container mb-2" id="logoPreviewContainer">
|
||||
@if($isEdit && $club->logo)
|
||||
<img src="{{ asset('storage/' . $club->logo) }}"
|
||||
id="logoPreview"
|
||||
class="cropper-preview-image"
|
||||
style="width: 150px; height: 150px; border-radius: 8px; border: 2px solid #dee2e6;">
|
||||
@else
|
||||
<div id="logoPreview"
|
||||
class="cropper-preview-placeholder"
|
||||
style="width: 150px; height: 150px; border-radius: 8px; border: 2px dashed #dee2e6; display: flex; align-items: center; justify-content: center; background-color: #f0f0f0; color: #6c757d;">
|
||||
<i class="bi bi-image" style="font-size: 2rem;"></i>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<input type="hidden" name="logo" id="logoInput" value="{{ $isEdit && $club->logo ? $club->logo : '' }}">
|
||||
<input type="hidden" name="logo_folder" value="clubs/logos">
|
||||
<input type="hidden" name="logo_filename" value="logo_{{ time() }}">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="openLogoCropper()">
|
||||
<i class="bi bi-camera me-2"></i>Upload Logo
|
||||
</button>
|
||||
<small class="text-muted d-block mt-2">Square image recommended (400x400px)</small>
|
||||
<small class="text-muted d-block">Used as main logo and favicon</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label d-block">Cover Image</label>
|
||||
<div class="text-center">
|
||||
<!-- Cover Preview -->
|
||||
<div class="cropper-preview-container mb-2" id="coverPreviewContainer">
|
||||
@if($isEdit && $club->cover_image)
|
||||
<img src="{{ asset('storage/' . $club->cover_image) }}"
|
||||
id="coverPreview"
|
||||
class="cropper-preview-image"
|
||||
style="width: 250px; height: 83px; border-radius: 8px; border: 2px solid #dee2e6;">
|
||||
@else
|
||||
<div id="coverPreview"
|
||||
class="cropper-preview-placeholder"
|
||||
style="width: 250px; height: 83px; border-radius: 8px; border: 2px dashed #dee2e6; display: flex; align-items: center; justify-content: center; background-color: #f0f0f0; color: #6c757d;">
|
||||
<i class="bi bi-image" style="font-size: 2rem;"></i>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<input type="hidden" name="cover_image" id="coverInput" value="{{ $isEdit && $club->cover_image ? $club->cover_image : '' }}">
|
||||
<input type="hidden" name="cover_image_folder" value="clubs/covers">
|
||||
<input type="hidden" name="cover_image_filename" value="cover_{{ time() }}">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="openCoverCropper()">
|
||||
<i class="bi bi-camera me-2"></i>Upload Cover
|
||||
</button>
|
||||
<small class="text-muted d-block mt-2">Wide banner image (1200x400px)</small>
|
||||
<small class="text-muted d-block">Used for club profile header</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PART 2: Internal Cropper Overlays (NOT separate modals) -->
|
||||
<!-- Logo Cropper Overlay -->
|
||||
<div id="logoCropperOverlay" class="cropper-overlay" style="display: none;">
|
||||
<div class="cropper-panel">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="mb-0">Crop Logo</h5>
|
||||
<button type="button" class="btn-close" onclick="closeLogoCropper()"></button>
|
||||
</div>
|
||||
|
||||
<input type="file" id="logoFileInput" class="form-control form-control-sm mb-3" accept="image/*">
|
||||
|
||||
<div id="logoBox" class="takeone-canvas" style="height: 400px; background: #111; border-radius: 8px;"></div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-6">
|
||||
<label class="form-label small">Zoom</label>
|
||||
<input type="range" class="form-range" id="logoZoom" min="0" max="100" step="1" value="0">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label small">Rotation</label>
|
||||
<input type="range" class="form-range" id="logoRotation" min="-180" max="180" step="1" value="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button type="button" class="btn btn-secondary flex-fill" onclick="closeLogoCropper()">Cancel</button>
|
||||
<button type="button" class="btn btn-primary flex-fill" onclick="saveLogoCrop()">Save & Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cover Cropper Overlay -->
|
||||
<div id="coverCropperOverlay" class="cropper-overlay" style="display: none;">
|
||||
<div class="cropper-panel">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="mb-0">Crop Cover Image</h5>
|
||||
<button type="button" class="btn-close" onclick="closeCoverCropper()"></button>
|
||||
</div>
|
||||
|
||||
<input type="file" id="coverFileInput" class="form-control form-control-sm mb-3" accept="image/*">
|
||||
|
||||
<div id="coverBox" class="takeone-canvas" style="height: 400px; background: #111; border-radius: 8px;"></div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-6">
|
||||
<label class="form-label small">Zoom</label>
|
||||
<input type="range" class="form-range" id="coverZoom" min="0" max="100" step="1" value="0">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label small">Rotation</label>
|
||||
<input type="range" class="form-range" id="coverRotation" min="-180" max="180" step="1" value="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button type="button" class="btn btn-secondary flex-fill" onclick="closeCoverCropper()">Cancel</button>
|
||||
<button type="button" class="btn btn-primary flex-fill" onclick="saveCoverCrop()">Save & Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social Media Links -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Social Media Links</label>
|
||||
<p class="text-muted small mb-3">Add links to your club's social media profiles</p>
|
||||
|
||||
<div id="socialLinksContainer">
|
||||
@if($isEdit && $club->socialLinks && $club->socialLinks->count() > 0)
|
||||
@foreach($club->socialLinks as $index => $link)
|
||||
<div class="social-link-row mb-3" data-index="{{ $index }}">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4">
|
||||
<select class="form-select" name="social_links[{{ $index }}][platform]" required>
|
||||
<option value="">Select Platform</option>
|
||||
<option value="facebook" {{ $link->platform === 'facebook' ? 'selected' : '' }}>
|
||||
<i class="bi bi-facebook"></i> Facebook
|
||||
</option>
|
||||
<option value="instagram" {{ $link->platform === 'instagram' ? 'selected' : '' }}>
|
||||
Instagram
|
||||
</option>
|
||||
<option value="twitter" {{ $link->platform === 'twitter' ? 'selected' : '' }}>
|
||||
X (Twitter)
|
||||
</option>
|
||||
<option value="tiktok" {{ $link->platform === 'tiktok' ? 'selected' : '' }}>
|
||||
TikTok
|
||||
</option>
|
||||
<option value="youtube" {{ $link->platform === 'youtube' ? 'selected' : '' }}>
|
||||
YouTube
|
||||
</option>
|
||||
<option value="whatsapp" {{ $link->platform === 'whatsapp' ? 'selected' : '' }}>
|
||||
WhatsApp
|
||||
</option>
|
||||
<option value="website" {{ $link->platform === 'website' ? 'selected' : '' }}>
|
||||
Website
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<input type="url"
|
||||
class="form-control"
|
||||
name="social_links[{{ $index }}][url]"
|
||||
value="{{ $link->url }}"
|
||||
placeholder="https://..."
|
||||
required>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="button" class="btn btn-outline-danger w-100" onclick="removeSocialLink(this)">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addSocialLink()">
|
||||
<i class="bi bi-plus-circle me-2"></i>Add Social Link
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
/* PART 2: Cropper Overlay Styles */
|
||||
.cropper-overlay {
|
||||
position: fixed; /* PART 1 FIX: Fixed positioning to cover entire modal */
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.85);
|
||||
z-index: 1065; /* PART 1 FIX: Higher than modal content (1060) */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.cropper-panel {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.takeone-canvas {
|
||||
position: relative;
|
||||
border: 1px solid #222;
|
||||
}
|
||||
|
||||
.cropper-preview-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.cropper-preview-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f0f0f0;
|
||||
border: 2px dashed #dee2e6;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.cropper-preview-image {
|
||||
object-fit: cover;
|
||||
border: 2px solid #dee2e6;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/cropme.min.css">
|
||||
<script src="https://unpkg.com/[email protected]/dist/cropme.min.js"></script>
|
||||
|
||||
<script>
|
||||
let socialLinkIndex = {{ $isEdit && $club->socialLinks ? $club->socialLinks->count() : 0 }};
|
||||
let qrCode = null;
|
||||
|
||||
// PART 2: Cropper instances
|
||||
let logoCropper = null;
|
||||
let coverCropper = null;
|
||||
const zoomMin = 0.01;
|
||||
const zoomMax = 3;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Update URL preview when slug changes
|
||||
const slugInput = document.getElementById('slug');
|
||||
const urlPreview = document.getElementById('clubUrlPreview');
|
||||
|
||||
if (slugInput && urlPreview) {
|
||||
slugInput.addEventListener('input', function() {
|
||||
const baseUrl = '{{ url("/club/") }}';
|
||||
const slug = this.value || 'your-club-slug';
|
||||
urlPreview.textContent = `${baseUrl}/${slug}`;
|
||||
|
||||
// Regenerate QR code
|
||||
generateQRCode(`${baseUrl}/${slug}`);
|
||||
});
|
||||
|
||||
// Mark slug as manually edited when user types
|
||||
slugInput.addEventListener('keydown', function() {
|
||||
this.dataset.manuallyEdited = 'true';
|
||||
});
|
||||
|
||||
// Initial QR code generation
|
||||
const initialUrl = urlPreview.textContent;
|
||||
generateQRCode(initialUrl);
|
||||
}
|
||||
});
|
||||
|
||||
// Generate QR Code
|
||||
function generateQRCode(url) {
|
||||
const container = document.getElementById('qrCodeContainer');
|
||||
if (!container) return;
|
||||
|
||||
// Clear existing QR code
|
||||
container.innerHTML = '';
|
||||
|
||||
// Generate new QR code
|
||||
qrCode = new QRCode(container, {
|
||||
text: url,
|
||||
width: 200,
|
||||
height: 200,
|
||||
colorDark: "#000000",
|
||||
colorLight: "#ffffff",
|
||||
correctLevel: QRCode.CorrectLevel.H
|
||||
});
|
||||
}
|
||||
|
||||
// Copy club URL to clipboard
|
||||
function copyClubUrl() {
|
||||
const urlPreview = document.getElementById('clubUrlPreview');
|
||||
if (!urlPreview) return;
|
||||
|
||||
const url = urlPreview.textContent;
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
// Show success feedback
|
||||
if (typeof Toast !== 'undefined') {
|
||||
Toast.success('Copied!', 'Club URL copied to clipboard');
|
||||
} else {
|
||||
alert('URL copied to clipboard!');
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy:', err);
|
||||
});
|
||||
}
|
||||
|
||||
// Download QR Code
|
||||
function downloadQRCode() {
|
||||
const container = document.getElementById('qrCodeContainer');
|
||||
if (!container) return;
|
||||
|
||||
const canvas = container.querySelector('canvas');
|
||||
if (!canvas) return;
|
||||
|
||||
const url = canvas.toDataURL('image/png');
|
||||
const link = document.createElement('a');
|
||||
link.download = 'club-qr-code.png';
|
||||
link.href = url;
|
||||
link.click();
|
||||
}
|
||||
|
||||
// Print QR Code
|
||||
function printQRCode() {
|
||||
const container = document.getElementById('qrCodeContainer');
|
||||
if (!container) return;
|
||||
|
||||
const canvas = container.querySelector('canvas');
|
||||
if (!canvas) return;
|
||||
|
||||
const printWindow = window.open('', '_blank');
|
||||
printWindow.document.write(`
|
||||
<html>
|
||||
<head>
|
||||
<title>Club QR Code</title>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
max-width: 400px;
|
||||
height: auto;
|
||||
}
|
||||
h2 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src="${canvas.toDataURL('image/png')}" alt="Club QR Code">
|
||||
<h2>${document.getElementById('club_name')?.value || 'Club QR Code'}</h2>
|
||||
<p>${document.getElementById('clubUrlPreview')?.textContent || ''}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
printWindow.document.close();
|
||||
printWindow.focus();
|
||||
setTimeout(() => {
|
||||
printWindow.print();
|
||||
printWindow.close();
|
||||
}, 250);
|
||||
}
|
||||
|
||||
// Add social link row
|
||||
function addSocialLink() {
|
||||
const container = document.getElementById('socialLinksContainer');
|
||||
if (!container) return;
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'social-link-row mb-3';
|
||||
row.dataset.index = socialLinkIndex;
|
||||
row.innerHTML = `
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4">
|
||||
<select class="form-select" name="social_links[${socialLinkIndex}][platform]" required>
|
||||
<option value="">Select Platform</option>
|
||||
<option value="facebook">Facebook</option>
|
||||
<option value="instagram">Instagram</option>
|
||||
<option value="twitter">X (Twitter)</option>
|
||||
<option value="tiktok">TikTok</option>
|
||||
<option value="youtube">YouTube</option>
|
||||
<option value="whatsapp">WhatsApp</option>
|
||||
<option value="website">Website</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<input type="url"
|
||||
class="form-control"
|
||||
name="social_links[${socialLinkIndex}][url]"
|
||||
placeholder="https://..."
|
||||
required>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="button" class="btn btn-outline-danger w-100" onclick="removeSocialLink(this)">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(row);
|
||||
socialLinkIndex++;
|
||||
}
|
||||
|
||||
// Remove social link row
|
||||
function removeSocialLink(button) {
|
||||
const row = button.closest('.social-link-row');
|
||||
if (row) {
|
||||
row.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// PART 2: Cropper Overlay Functions
|
||||
// ============================================
|
||||
|
||||
// Helper function to apply transform
|
||||
function applyTransform(instance) {
|
||||
if (!instance.properties.image) return;
|
||||
const p = instance.properties;
|
||||
const t = `translate3d(${p.x}px, ${p.y}px, 0) scale(${p.scale}) rotate(${p.deg}deg)`;
|
||||
p.image.style.transform = t;
|
||||
}
|
||||
|
||||
// Initialize cropper with specific aspect ratio
|
||||
function initCropper(elementId, width, height, shape, aspectRatio) {
|
||||
const el = document.getElementById(elementId);
|
||||
if (!el) return null;
|
||||
|
||||
const cropper = new Cropme(el, {
|
||||
container: { width: '100%', height: 400 },
|
||||
viewport: {
|
||||
width: width,
|
||||
height: height,
|
||||
type: shape,
|
||||
border: { enable: true, width: 2, color: '#fff' }
|
||||
},
|
||||
transformOrigin: 'viewport',
|
||||
zoom: { min: zoomMin, max: zoomMax, enable: true, mouseWheel: true, slider: false },
|
||||
rotation: { enable: true, slider: false }
|
||||
});
|
||||
|
||||
return cropper;
|
||||
}
|
||||
|
||||
// ===== LOGO CROPPER =====
|
||||
function openLogoCropper() {
|
||||
const overlay = document.getElementById('logoCropperOverlay');
|
||||
overlay.style.display = 'flex';
|
||||
|
||||
// Prevent main modal body from scrolling
|
||||
const modalBody = document.querySelector('#clubModal .modal-body');
|
||||
if (modalBody) modalBody.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeLogoCropper() {
|
||||
const overlay = document.getElementById('logoCropperOverlay');
|
||||
overlay.style.display = 'none';
|
||||
|
||||
// Restore main modal body scrolling
|
||||
const modalBody = document.querySelector('#clubModal .modal-body');
|
||||
if (modalBody) modalBody.style.overflow = 'auto';
|
||||
|
||||
// Destroy cropper
|
||||
if (logoCropper) {
|
||||
logoCropper.destroy();
|
||||
logoCropper = null;
|
||||
}
|
||||
|
||||
// Reset file input
|
||||
document.getElementById('logoFileInput').value = '';
|
||||
}
|
||||
|
||||
function saveLogoCrop() {
|
||||
if (!logoCropper) return;
|
||||
|
||||
// Logo: Crop to 400x400 square
|
||||
logoCropper.crop({
|
||||
type: 'base64',
|
||||
width: 400,
|
||||
height: 400
|
||||
}).then(base64 => {
|
||||
// Store in hidden input
|
||||
document.getElementById('logoInput').value = base64;
|
||||
|
||||
// Update preview
|
||||
const preview = document.getElementById('logoPreview');
|
||||
if (preview && preview.tagName === 'IMG') {
|
||||
preview.src = base64;
|
||||
} else {
|
||||
const container = document.getElementById('logoPreviewContainer');
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<img src="${base64}"
|
||||
id="logoPreview"
|
||||
class="cropper-preview-image"
|
||||
style="width: 150px; height: 150px; border-radius: 8px; border: 2px solid #dee2e6;">
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Close overlay
|
||||
closeLogoCropper();
|
||||
});
|
||||
}
|
||||
|
||||
// Logo file input handler
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const logoFileInput = document.getElementById('logoFileInput');
|
||||
if (logoFileInput) {
|
||||
logoFileInput.addEventListener('change', function() {
|
||||
if (this.files && this.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
if (logoCropper) logoCropper.destroy();
|
||||
// Logo: Square aspect ratio (400x400)
|
||||
logoCropper = initCropper('logoBox', 400, 400, 'square', 1);
|
||||
if (logoCropper) {
|
||||
logoCropper.bind({ url: event.target.result }).then(() => {
|
||||
document.getElementById('logoZoom').value = 0;
|
||||
document.getElementById('logoRotation').value = 0;
|
||||
});
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(this.files[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Logo zoom handler
|
||||
const logoZoom = document.getElementById('logoZoom');
|
||||
if (logoZoom) {
|
||||
logoZoom.addEventListener('input', function() {
|
||||
if (!logoCropper || !logoCropper.properties.image) return;
|
||||
const p = parseFloat(this.value);
|
||||
const scale = zoomMin + (zoomMax - zoomMin) * (p / 100);
|
||||
logoCropper.properties.scale = Math.min(Math.max(scale, zoomMin), zoomMax);
|
||||
applyTransform(logoCropper);
|
||||
});
|
||||
}
|
||||
|
||||
// Logo rotation handler
|
||||
const logoRotation = document.getElementById('logoRotation');
|
||||
if (logoRotation) {
|
||||
logoRotation.addEventListener('input', function() {
|
||||
if (logoCropper) {
|
||||
logoCropper.rotate(parseInt(this.value, 10));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ===== COVER CROPPER =====
|
||||
function openCoverCropper() {
|
||||
const overlay = document.getElementById('coverCropperOverlay');
|
||||
overlay.style.display = 'flex';
|
||||
|
||||
// Prevent main modal body from scrolling
|
||||
const modalBody = document.querySelector('#clubModal .modal-body');
|
||||
if (modalBody) modalBody.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeCoverCropper() {
|
||||
const overlay = document.getElementById('coverCropperOverlay');
|
||||
overlay.style.display = 'none';
|
||||
|
||||
// Restore main modal body scrolling
|
||||
const modalBody = document.querySelector('#clubModal .modal-body');
|
||||
if (modalBody) modalBody.style.overflow = 'auto';
|
||||
|
||||
// Destroy cropper
|
||||
if (coverCropper) {
|
||||
coverCropper.destroy();
|
||||
coverCropper = null;
|
||||
}
|
||||
|
||||
// Reset file input
|
||||
document.getElementById('coverFileInput').value = '';
|
||||
}
|
||||
|
||||
function saveCoverCrop() {
|
||||
if (!coverCropper) return;
|
||||
|
||||
// Cover: Crop to 1200x400 wide banner (3:1 aspect ratio)
|
||||
coverCropper.crop({
|
||||
type: 'base64',
|
||||
width: 1200,
|
||||
height: 400
|
||||
}).then(base64 => {
|
||||
// Store in hidden input for COVER (not logo!)
|
||||
document.getElementById('coverInput').value = base64;
|
||||
|
||||
// Update COVER preview (not logo!)
|
||||
const preview = document.getElementById('coverPreview');
|
||||
if (preview && preview.tagName === 'IMG') {
|
||||
preview.src = base64;
|
||||
} else {
|
||||
const container = document.getElementById('coverPreviewContainer');
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<img src="${base64}"
|
||||
id="coverPreview"
|
||||
class="cropper-preview-image"
|
||||
style="width: 250px; height: 83px; border-radius: 8px; border: 2px solid #dee2e6;">
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Close overlay
|
||||
closeCoverCropper();
|
||||
});
|
||||
}
|
||||
|
||||
// Cover file input handler
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const coverFileInput = document.getElementById('coverFileInput');
|
||||
if (coverFileInput) {
|
||||
coverFileInput.addEventListener('change', function() {
|
||||
if (this.files && this.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
if (coverCropper) coverCropper.destroy();
|
||||
// Cover: Wide banner aspect ratio (600x200 = 3:1) - fits in container
|
||||
// Will be scaled up to 1200x400 on save
|
||||
coverCropper = initCropper('coverBox', 600, 200, 'square', 3);
|
||||
if (coverCropper) {
|
||||
coverCropper.bind({ url: event.target.result }).then(() => {
|
||||
document.getElementById('coverZoom').value = 0;
|
||||
document.getElementById('coverRotation').value = 0;
|
||||
});
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(this.files[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Cover zoom handler
|
||||
const coverZoom = document.getElementById('coverZoom');
|
||||
if (coverZoom) {
|
||||
coverZoom.addEventListener('input', function() {
|
||||
if (!coverCropper || !coverCropper.properties.image) return;
|
||||
const p = parseFloat(this.value);
|
||||
const scale = zoomMin + (zoomMax - zoomMin) * (p / 100);
|
||||
coverCropper.properties.scale = Math.min(Math.max(scale, zoomMin), zoomMax);
|
||||
applyTransform(coverCropper);
|
||||
});
|
||||
}
|
||||
|
||||
// Cover rotation handler
|
||||
const coverRotation = document.getElementById('coverRotation');
|
||||
if (coverRotation) {
|
||||
coverRotation.addEventListener('input', function() {
|
||||
if (coverCropper) {
|
||||
coverCropper.rotate(parseInt(this.value, 10));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,541 @@
|
||||
@props(['club' => null, 'mode' => 'create'])
|
||||
|
||||
@php
|
||||
$isEdit = $mode === 'edit' && $club;
|
||||
@endphp
|
||||
|
||||
<div class="container-fluid px-0">
|
||||
<h5 class="fw-bold mb-3">Location</h5>
|
||||
<p class="text-muted mb-4">Set your club's geographic location and regional settings</p>
|
||||
|
||||
<!-- Country, Timezone, Currency Row -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<x-nationality-dropdown
|
||||
name="country"
|
||||
id="country"
|
||||
label="Country"
|
||||
:value="$club->country ?? old('country', 'Bahrain')"
|
||||
:required="true"
|
||||
:error="null" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<x-timezone-dropdown-bootstrap
|
||||
name="timezone"
|
||||
id="timezone"
|
||||
:value="$club->timezone ?? old('timezone', 'Asia/Bahrain')"
|
||||
:required="false"
|
||||
:error="null" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<x-currency-dropdown-bootstrap
|
||||
name="currency"
|
||||
id="currency"
|
||||
:value="$club->currency ?? old('currency', 'BHD')"
|
||||
:required="false"
|
||||
:error="null" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address -->
|
||||
<div class="mb-4">
|
||||
<label for="address" class="form-label">Street Address</label>
|
||||
<textarea class="form-control"
|
||||
id="address"
|
||||
name="address"
|
||||
rows="2"
|
||||
placeholder="Enter the full street address of your club">{{ $club->address ?? old('address') }}</textarea>
|
||||
<small class="text-muted">Full address including building number, street name, area, etc.</small>
|
||||
</div>
|
||||
|
||||
<!-- Map -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Location on Map</label>
|
||||
<div id="modalClubMap" style="height: 400px; border-radius: 0.5rem; border: 1px solid hsl(var(--border));"></div>
|
||||
<small class="text-muted">Drag the marker to set the exact location of your club</small>
|
||||
</div>
|
||||
|
||||
<!-- GPS Coordinates -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label for="gps_lat" class="form-label">
|
||||
<i class="bi bi-geo-alt me-1"></i>Latitude
|
||||
</label>
|
||||
<input type="number"
|
||||
class="form-control"
|
||||
id="gps_lat"
|
||||
name="gps_lat"
|
||||
value="{{ $club->gps_lat ?? old('gps_lat') }}"
|
||||
step="0.0000001"
|
||||
min="-90"
|
||||
max="90"
|
||||
placeholder="e.g., 26.0667">
|
||||
<small class="text-muted">Decimal degrees (-90 to 90)</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="gps_long" class="form-label">
|
||||
<i class="bi bi-geo-alt me-1"></i>Longitude
|
||||
</label>
|
||||
<input type="number"
|
||||
class="form-control"
|
||||
id="gps_long"
|
||||
name="gps_long"
|
||||
value="{{ $club->gps_long ?? old('gps_long') }}"
|
||||
step="0.0000001"
|
||||
min="-180"
|
||||
max="180"
|
||||
placeholder="e.g., 50.5577">
|
||||
<small class="text-muted">Decimal degrees (-180 to 180)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Google Maps Link -->
|
||||
<div class="mb-4">
|
||||
<label for="google_maps_link" class="form-label">
|
||||
<i class="bi bi-google me-1"></i>Google Maps Link (Optional)
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-white">
|
||||
<i class="bi bi-link-45deg"></i>
|
||||
</span>
|
||||
<input type="url"
|
||||
class="form-control"
|
||||
id="google_maps_link"
|
||||
name="google_maps_link"
|
||||
placeholder="Paste Google Maps share link here..."
|
||||
pattern="https?://.*google\.com/maps.*|https?://goo\.gl/maps/.*">
|
||||
</div>
|
||||
<small class="text-muted">Paste a Google Maps share URL to auto-fill coordinates</small>
|
||||
</div>
|
||||
|
||||
<!-- Quick Location Actions -->
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="getCurrentLocation()">
|
||||
<i class="bi bi-crosshair me-2"></i>Use My Current Location
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="centerOnCountry()">
|
||||
<i class="bi bi-globe me-2"></i>Center on Selected Country
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
let clubMap = null;
|
||||
let clubMarker = null;
|
||||
let countriesData = null;
|
||||
let mapInitialized = false;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Load countries data
|
||||
fetch('/data/countries.json')
|
||||
.then(response => response.json())
|
||||
.then(countries => {
|
||||
countriesData = countries;
|
||||
|
||||
// PART 1A: Device-based preselection on modal open (create mode only)
|
||||
const clubForm = document.getElementById('clubForm');
|
||||
if (clubForm && clubForm.dataset.mode === 'create') {
|
||||
detectAndPreselectCountries(countries);
|
||||
}
|
||||
|
||||
setupLocationHandlers();
|
||||
})
|
||||
.catch(error => console.error('Error loading countries:', error));
|
||||
|
||||
// Initialize map when location tab is shown OR when modal opens with location tab active
|
||||
const locationTab = document.getElementById('location-tab');
|
||||
const clubModal = document.getElementById('clubModal');
|
||||
|
||||
// Function to initialize map if not already done
|
||||
function tryInitializeMap() {
|
||||
if (!mapInitialized && locationTab && locationTab.classList.contains('active')) {
|
||||
setTimeout(() => {
|
||||
initializeMap();
|
||||
mapInitialized = true;
|
||||
}, 100);
|
||||
} else if (clubMap) {
|
||||
// ISSUE 4 FIX: Fix gray tiles by invalidating size
|
||||
setTimeout(() => {
|
||||
clubMap.invalidateSize();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for tab shown event
|
||||
if (locationTab) {
|
||||
locationTab.addEventListener('shown.bs.tab', tryInitializeMap);
|
||||
}
|
||||
|
||||
// Listen for modal shown event (for edit mode when location tab is already active)
|
||||
if (clubModal) {
|
||||
clubModal.addEventListener('shown.bs.modal', function() {
|
||||
// Reset map initialization flag when modal opens
|
||||
mapInitialized = false;
|
||||
// Try to initialize map after modal is fully shown
|
||||
setTimeout(tryInitializeMap, 150);
|
||||
});
|
||||
|
||||
// Clean up map when modal closes
|
||||
clubModal.addEventListener('hidden.bs.modal', function() {
|
||||
if (clubMap) {
|
||||
clubMap.remove();
|
||||
clubMap = null;
|
||||
clubMarker = null;
|
||||
mapInitialized = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// PART 1A: Detect device location and preselect country/timezone/currency
|
||||
function detectAndPreselectCountries(countries) {
|
||||
if (!navigator.geolocation) {
|
||||
// Fallback to default (Bahrain)
|
||||
preselectCountryData('Bahrain', countries);
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
function(position) {
|
||||
const lat = position.coords.latitude;
|
||||
const lon = position.coords.longitude;
|
||||
|
||||
// Use reverse geocoding to get country
|
||||
fetch(`https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lon}&localityLanguage=en`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const countryName = data.countryName || 'Bahrain';
|
||||
preselectCountryData(countryName, countries);
|
||||
})
|
||||
.catch(() => {
|
||||
// Fallback to default
|
||||
preselectCountryData('Bahrain', countries);
|
||||
});
|
||||
},
|
||||
function() {
|
||||
// Geolocation denied or failed, use default
|
||||
preselectCountryData('Bahrain', countries);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Preselect country, timezone, and currency based on detected/default country
|
||||
function preselectCountryData(countryName, countries) {
|
||||
const country = countries.find(c => c.name.toLowerCase() === countryName.toLowerCase());
|
||||
if (!country) return;
|
||||
|
||||
// Preselect country
|
||||
const countryInput = document.getElementById('country');
|
||||
if (countryInput && !countryInput.value) {
|
||||
countryInput.value = country.iso3 || country.name;
|
||||
countryInput.dispatchEvent(new Event('change'));
|
||||
}
|
||||
|
||||
// Preselect timezone using Bootstrap dropdown
|
||||
const timezoneInput = document.getElementById('timezone');
|
||||
if (timezoneInput && country.timezone && !timezoneInput.value && typeof setTimezoneValue === 'function') {
|
||||
setTimezoneValue('timezone', country.timezone, countries);
|
||||
}
|
||||
|
||||
// Preselect currency using Bootstrap dropdown
|
||||
const currencyInput = document.getElementById('currency');
|
||||
if (currencyInput && country.currency && !currencyInput.value && typeof setCurrencyValue === 'function') {
|
||||
setCurrencyValue('currency', country.currency, countries);
|
||||
}
|
||||
|
||||
// Set map center to country
|
||||
if (country.latitude && country.longitude) {
|
||||
const lat = parseFloat(country.latitude);
|
||||
const lng = parseFloat(country.longitude);
|
||||
if (!isNaN(lat) && !isNaN(lng)) {
|
||||
const latInput = document.getElementById('gps_lat');
|
||||
const lngInput = document.getElementById('gps_long');
|
||||
if (latInput && !latInput.value) latInput.value = lat.toFixed(7);
|
||||
if (lngInput && !lngInput.value) lngInput.value = lng.toFixed(7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Leaflet map
|
||||
function initializeMap() {
|
||||
const mapElement = document.getElementById('modalClubMap');
|
||||
if (!mapElement) return;
|
||||
|
||||
const lat = parseFloat(document.getElementById('gps_lat')?.value) || 26.0667;
|
||||
const lng = parseFloat(document.getElementById('gps_long')?.value) || 50.5577;
|
||||
|
||||
// ISSUE 4 FIX: Initialize map without attribution control
|
||||
clubMap = L.map('clubMap', {
|
||||
attributionControl: false // Disable attribution
|
||||
}).setView([lat, lng], 13);
|
||||
|
||||
// ISSUE 4 FIX: Add tile layer without attribution text
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '', // Empty attribution
|
||||
maxZoom: 19
|
||||
}).addTo(clubMap);
|
||||
|
||||
// Add draggable marker
|
||||
clubMarker = L.marker([lat, lng], {
|
||||
draggable: true
|
||||
}).addTo(clubMap);
|
||||
|
||||
// Update coordinates when marker is dragged
|
||||
clubMarker.on('dragend', function(e) {
|
||||
const position = e.target.getLatLng();
|
||||
updateCoordinates(position.lat, position.lng);
|
||||
});
|
||||
|
||||
// Update marker when coordinates are manually entered
|
||||
const latInput = document.getElementById('gps_lat');
|
||||
const lngInput = document.getElementById('gps_long');
|
||||
|
||||
if (latInput && lngInput) {
|
||||
latInput.addEventListener('change', function() {
|
||||
updateMarkerPosition();
|
||||
});
|
||||
lngInput.addEventListener('change', function() {
|
||||
updateMarkerPosition();
|
||||
});
|
||||
}
|
||||
|
||||
// ISSUE 4 FIX: Fix initial rendering after a short delay
|
||||
setTimeout(() => {
|
||||
if (clubMap) {
|
||||
clubMap.invalidateSize();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Update coordinates in inputs
|
||||
function updateCoordinates(lat, lng) {
|
||||
const latInput = document.getElementById('gps_lat');
|
||||
const lngInput = document.getElementById('gps_long');
|
||||
|
||||
if (latInput) latInput.value = lat.toFixed(7);
|
||||
if (lngInput) lngInput.value = lng.toFixed(7);
|
||||
}
|
||||
|
||||
// Update marker position from inputs
|
||||
function updateMarkerPosition() {
|
||||
const lat = parseFloat(document.getElementById('gps_lat')?.value);
|
||||
const lng = parseFloat(document.getElementById('gps_long')?.value);
|
||||
|
||||
if (!isNaN(lat) && !isNaN(lng) && clubMarker && clubMap) {
|
||||
const newPos = L.latLng(lat, lng);
|
||||
clubMarker.setLatLng(newPos);
|
||||
clubMap.setView(newPos, clubMap.getZoom());
|
||||
}
|
||||
}
|
||||
|
||||
// Setup location-related event handlers
|
||||
function setupLocationHandlers() {
|
||||
// Watch for country changes
|
||||
const countryInput = document.getElementById('country');
|
||||
if (countryInput) {
|
||||
// Use MutationObserver to detect value changes
|
||||
const observer = new MutationObserver(function(mutations) {
|
||||
mutations.forEach(function(mutation) {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
|
||||
handleCountryChange(countryInput.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe(countryInput, { attributes: true });
|
||||
|
||||
// Also listen for direct changes
|
||||
countryInput.addEventListener('change', function() {
|
||||
handleCountryChange(this.value);
|
||||
});
|
||||
|
||||
// Check periodically for value changes (fallback)
|
||||
let lastCountryValue = countryInput.value;
|
||||
setInterval(function() {
|
||||
if (countryInput.value !== lastCountryValue) {
|
||||
lastCountryValue = countryInput.value;
|
||||
handleCountryChange(countryInput.value);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Parse Google Maps link
|
||||
const googleMapsInput = document.getElementById('google_maps_link');
|
||||
if (googleMapsInput) {
|
||||
googleMapsInput.addEventListener('change', function() {
|
||||
parseGoogleMapsLink(this.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// PART 1B: Handle country change - update timezone, currency, and map
|
||||
function handleCountryChange(countryName) {
|
||||
if (!countriesData || !countryName) return;
|
||||
|
||||
const country = countriesData.find(c =>
|
||||
c.name.toLowerCase() === countryName.toLowerCase() ||
|
||||
c.iso3 === countryName
|
||||
);
|
||||
|
||||
if (!country) return;
|
||||
|
||||
// PART 1B: Always update timezone to match country using Bootstrap dropdown
|
||||
if (country.timezone && typeof setTimezoneValue === 'function') {
|
||||
setTimezoneValue('timezone', country.timezone, countriesData);
|
||||
}
|
||||
|
||||
// PART 1B: Always update currency to match country using Bootstrap dropdown
|
||||
if (country.currency && typeof setCurrencyValue === 'function') {
|
||||
setCurrencyValue('currency', country.currency, countriesData);
|
||||
}
|
||||
|
||||
// Center map on country
|
||||
if (country.latitude && country.longitude) {
|
||||
const lat = parseFloat(country.latitude);
|
||||
const lng = parseFloat(country.longitude);
|
||||
|
||||
if (!isNaN(lat) && !isNaN(lng)) {
|
||||
// Update coordinates
|
||||
const latInput = document.getElementById('gps_lat');
|
||||
const lngInput = document.getElementById('gps_long');
|
||||
|
||||
// Only update if empty or user wants to recenter
|
||||
const shouldUpdate = !latInput?.value || !lngInput?.value;
|
||||
|
||||
if (shouldUpdate) {
|
||||
updateCoordinates(lat, lng);
|
||||
}
|
||||
|
||||
// Always recenter map view on country
|
||||
if (clubMarker && clubMap) {
|
||||
if (shouldUpdate) {
|
||||
clubMarker.setLatLng([lat, lng]);
|
||||
}
|
||||
clubMap.setView([lat, lng], 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get current location using browser geolocation
|
||||
function getCurrentLocation() {
|
||||
if (!navigator.geolocation) {
|
||||
alert('Geolocation is not supported by your browser');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
const btn = event.target.closest('button');
|
||||
const originalHtml = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Getting location...';
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
function(position) {
|
||||
const lat = position.coords.latitude;
|
||||
const lng = position.coords.longitude;
|
||||
|
||||
updateCoordinates(lat, lng);
|
||||
if (clubMarker && clubMap) {
|
||||
clubMarker.setLatLng([lat, lng]);
|
||||
clubMap.setView([lat, lng], 15);
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalHtml;
|
||||
|
||||
if (typeof Toast !== 'undefined') {
|
||||
Toast.success('Success', 'Location updated successfully');
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
console.error('Geolocation error:', error);
|
||||
alert('Unable to get your location. Please check your browser permissions.');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalHtml;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Center map on selected country
|
||||
function centerOnCountry() {
|
||||
const countryInput = document.getElementById('country');
|
||||
if (!countryInput || !countriesData) return;
|
||||
|
||||
const countryName = countryInput.value;
|
||||
const country = countriesData.find(c =>
|
||||
c.name.toLowerCase() === countryName.toLowerCase() ||
|
||||
c.iso3 === countryName
|
||||
);
|
||||
|
||||
if (country && country.latitude && country.longitude) {
|
||||
const lat = parseFloat(country.latitude);
|
||||
const lng = parseFloat(country.longitude);
|
||||
|
||||
if (!isNaN(lat) && !isNaN(lng) && clubMap) {
|
||||
clubMap.setView([lat, lng], 10);
|
||||
|
||||
if (typeof Toast !== 'undefined') {
|
||||
Toast.info('Info', `Centered on ${country.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Google Maps link to extract coordinates
|
||||
function parseGoogleMapsLink(url) {
|
||||
if (!url) return;
|
||||
|
||||
try {
|
||||
// Try to extract coordinates from various Google Maps URL formats
|
||||
let lat, lng;
|
||||
|
||||
// Format: @lat,lng
|
||||
const atMatch = url.match(/@(-?\d+\.\d+),(-?\d+\.\d+)/);
|
||||
if (atMatch) {
|
||||
lat = parseFloat(atMatch[1]);
|
||||
lng = parseFloat(atMatch[2]);
|
||||
}
|
||||
|
||||
// Format: !3d and !4d
|
||||
if (!lat || !lng) {
|
||||
const dMatch = url.match(/!3d(-?\d+\.\d+)!4d(-?\d+\.\d+)/);
|
||||
if (dMatch) {
|
||||
lat = parseFloat(dMatch[1]);
|
||||
lng = parseFloat(dMatch[2]);
|
||||
}
|
||||
}
|
||||
|
||||
// Format: ll=lat,lng
|
||||
if (!lat || !lng) {
|
||||
const llMatch = url.match(/ll=(-?\d+\.\d+),(-?\d+\.\d+)/);
|
||||
if (llMatch) {
|
||||
lat = parseFloat(llMatch[1]);
|
||||
lng = parseFloat(llMatch[2]);
|
||||
}
|
||||
}
|
||||
|
||||
if (lat && lng && !isNaN(lat) && !isNaN(lng)) {
|
||||
updateCoordinates(lat, lng);
|
||||
if (clubMarker && clubMap) {
|
||||
clubMarker.setLatLng([lat, lng]);
|
||||
clubMap.setView([lat, lng], 15);
|
||||
}
|
||||
|
||||
if (typeof Toast !== 'undefined') {
|
||||
Toast.success('Success', 'Coordinates extracted from Google Maps link');
|
||||
}
|
||||
} else {
|
||||
if (typeof Toast !== 'undefined') {
|
||||
Toast.warning('Warning', 'Could not extract coordinates from the link');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing Google Maps link:', error);
|
||||
if (typeof Toast !== 'undefined') {
|
||||
Toast.error('Error', 'Invalid Google Maps link');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,204 @@
|
||||
@props(['name' => 'currency', 'id' => 'currency', 'value' => '', 'required' => false, 'error' => null, 'label' => 'Currency'])
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ $id }}" class="form-label">{{ $label }}</label>
|
||||
<div class="dropdown w-100" onclick="event.stopPropagation()">
|
||||
<button class="form-select dropdown-toggle d-flex align-items-center justify-content-between @error($name) is-invalid @enderror"
|
||||
type="button"
|
||||
id="{{ $id }}Dropdown"
|
||||
data-bs-toggle="dropdown"
|
||||
data-bs-auto-close="outside"
|
||||
aria-expanded="false"
|
||||
style="text-align: left; background-color: rgba(255,255,255,0.8);">
|
||||
<span class="d-flex align-items-center">
|
||||
<span id="{{ $id }}SelectedFlag"></span>
|
||||
<span class="currency-label" id="{{ $id }}SelectedCurrency">Select Currency</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="dropdown-menu p-2 w-100" aria-labelledby="{{ $id }}Dropdown" onclick="event.stopPropagation()">
|
||||
<input type="text"
|
||||
class="form-control form-control-sm mb-2"
|
||||
placeholder="Search currency..."
|
||||
id="{{ $id }}Search"
|
||||
onmousedown="event.stopPropagation()"
|
||||
onfocus="event.stopPropagation()"
|
||||
oninput="event.stopPropagation()"
|
||||
onkeydown="event.stopPropagation()"
|
||||
onkeyup="event.stopPropagation()">
|
||||
|
||||
<div class="currency-list" id="{{ $id }}List" data-component-id="{{ $id }}" style="max-height: 300px; overflow-y: auto;">
|
||||
<!-- Currencies will be populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="{{ $id }}" name="{{ $name }}" value="{{ $value }}" {{ $required ? 'required' : '' }}>
|
||||
</div>
|
||||
|
||||
@if($error)
|
||||
<span class="invalid-feedback d-block" role="alert">
|
||||
<strong>{{ $error }}</strong>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@once
|
||||
@push('styles')
|
||||
<style>
|
||||
.currency-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.currency-list .dropdown-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.currency-list .dropdown-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Load countries from JSON file
|
||||
fetch('/data/countries.json')
|
||||
.then(response => response.json())
|
||||
.then(countries => {
|
||||
// Initialize all currency dropdowns
|
||||
document.querySelectorAll('.currency-list').forEach(function(listElement) {
|
||||
const componentId = listElement.getAttribute('data-component-id');
|
||||
initializeCurrencyDropdown(componentId, countries);
|
||||
});
|
||||
})
|
||||
.catch(error => console.error('Error loading countries:', error));
|
||||
|
||||
function initializeCurrencyDropdown(componentId, countries) {
|
||||
const currencyList = document.getElementById(componentId + 'List');
|
||||
if (!currencyList) return;
|
||||
|
||||
// Clear existing items
|
||||
currencyList.innerHTML = '';
|
||||
|
||||
// Get unique currencies with their associated countries
|
||||
const currencyMap = {};
|
||||
countries.forEach(country => {
|
||||
if (country.currency && !currencyMap[country.currency]) {
|
||||
currencyMap[country.currency] = {
|
||||
currency: country.currency,
|
||||
flag: country.iso2,
|
||||
countryName: country.name
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Populate currency dropdown
|
||||
Object.values(currencyMap).forEach(currData => {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'dropdown-item d-flex align-items-center';
|
||||
button.type = 'button';
|
||||
button.setAttribute('data-currency-code', currData.currency);
|
||||
button.setAttribute('data-country-name', currData.countryName);
|
||||
button.setAttribute('data-flag', currData.flag);
|
||||
button.setAttribute('data-search', `${currData.countryName.toLowerCase()} ${currData.currency.toLowerCase()}`);
|
||||
|
||||
// Convert flag code to emoji
|
||||
const flagEmoji = currData.flag
|
||||
.toUpperCase()
|
||||
.split('')
|
||||
.map(char => String.fromCodePoint(127397 + char.charCodeAt(0)))
|
||||
.join('');
|
||||
|
||||
// Format: "🇧🇭 Bahrain – BHD"
|
||||
button.innerHTML = `
|
||||
<span class="me-2">${flagEmoji}</span>
|
||||
<span>${currData.countryName} – ${currData.currency}</span>
|
||||
`;
|
||||
button.addEventListener('click', function() {
|
||||
selectCurrency(componentId, currData.currency, flagEmoji, currData.countryName);
|
||||
});
|
||||
currencyList.appendChild(button);
|
||||
});
|
||||
|
||||
// Search functionality
|
||||
const searchInput = document.getElementById(componentId + 'Search');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function(e) {
|
||||
const searchTerm = e.target.value.toLowerCase();
|
||||
const items = currencyList.querySelectorAll('.dropdown-item');
|
||||
items.forEach(item => {
|
||||
const searchText = item.getAttribute('data-search') || '';
|
||||
if (searchText.includes(searchTerm)) {
|
||||
item.classList.remove('d-none');
|
||||
} else {
|
||||
item.classList.add('d-none');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Set initial value if provided
|
||||
const hiddenInput = document.getElementById(componentId);
|
||||
if (hiddenInput && hiddenInput.value) {
|
||||
const initialCurrency = Object.values(currencyMap).find(curr => curr.currency === hiddenInput.value);
|
||||
if (initialCurrency) {
|
||||
const flagEmoji = initialCurrency.flag
|
||||
.toUpperCase()
|
||||
.split('')
|
||||
.map(char => String.fromCodePoint(127397 + char.charCodeAt(0)))
|
||||
.join('');
|
||||
selectCurrency(componentId, initialCurrency.currency, flagEmoji, initialCurrency.countryName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectCurrency(componentId, currency, flag, countryName) {
|
||||
const flagElement = document.getElementById(componentId + 'SelectedFlag');
|
||||
const currencyElement = document.getElementById(componentId + 'SelectedCurrency');
|
||||
const hiddenInput = document.getElementById(componentId);
|
||||
|
||||
if (flagElement) flagElement.textContent = flag + ' ';
|
||||
if (currencyElement) currencyElement.textContent = `${countryName} – ${currency}`;
|
||||
if (hiddenInput) {
|
||||
hiddenInput.value = currency;
|
||||
// Trigger change event for other handlers
|
||||
hiddenInput.dispatchEvent(new Event('change'));
|
||||
}
|
||||
|
||||
// Close the dropdown after selection
|
||||
const dropdownButton = document.getElementById(componentId + 'Dropdown');
|
||||
if (dropdownButton) {
|
||||
const dropdown = bootstrap.Dropdown.getInstance(dropdownButton);
|
||||
if (dropdown) dropdown.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Expose function globally for external use
|
||||
window.setCurrencyValue = function(componentId, currency, countries) {
|
||||
const currencyMap = {};
|
||||
countries.forEach(country => {
|
||||
if (country.currency && !currencyMap[country.currency]) {
|
||||
currencyMap[country.currency] = {
|
||||
currency: country.currency,
|
||||
flag: country.iso2,
|
||||
countryName: country.name
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const currData = currencyMap[currency];
|
||||
if (currData) {
|
||||
const flagEmoji = currData.flag
|
||||
.toUpperCase()
|
||||
.split('')
|
||||
.map(char => String.fromCodePoint(127397 + char.charCodeAt(0)))
|
||||
.join('');
|
||||
selectCurrency(componentId, currency, flagEmoji, currData.countryName);
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
@@ -42,12 +42,14 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Populate dropdown
|
||||
// Populate dropdown with enhanced format: Flag + Country Name – Currency Code
|
||||
Object.values(uniqueCurrencies).forEach(currencyData => {
|
||||
const option = document.createElement('option');
|
||||
option.value = currencyData.currency;
|
||||
option.textContent = `${currencyData.currency} - ${currencyData.name} ${currencyData.currency_symbol}`;
|
||||
// Format: "Bahrain – BHD"
|
||||
option.textContent = `${currencyData.name} – ${currencyData.currency}`;
|
||||
option.setAttribute('data-flag', currencyData.flag);
|
||||
option.setAttribute('data-country', currencyData.name);
|
||||
selectElement.appendChild(option);
|
||||
});
|
||||
|
||||
@@ -56,7 +58,7 @@
|
||||
selectElement.value = initialValue;
|
||||
}
|
||||
|
||||
// Initialize Select2 for searchable dropdown
|
||||
// Initialize Select2 for searchable dropdown with flags
|
||||
if (typeof $ !== 'undefined' && $.fn.select2) {
|
||||
$(selectElement).select2({
|
||||
templateResult: function(state) {
|
||||
@@ -65,7 +67,9 @@
|
||||
}
|
||||
const option = $(state.element);
|
||||
const flagCode = option.data('flag');
|
||||
return $(`<span><span class="fi fi-${flagCode} me-2"></span>${state.text}</span>`);
|
||||
// Show flag emoji + text
|
||||
const flagEmoji = flagCode ? String.fromCodePoint(...[...flagCode.toUpperCase()].map(c => 127397 + c.charCodeAt())) : '';
|
||||
return $(`<span>${flagEmoji} ${state.text}</span>`);
|
||||
},
|
||||
templateSelection: function(state) {
|
||||
if (!state.id) {
|
||||
@@ -73,9 +77,25 @@
|
||||
}
|
||||
const option = $(state.element);
|
||||
const flagCode = option.data('flag');
|
||||
return $(`<span><span class="fi fi-${flagCode} me-2"></span>${state.text}</span>`);
|
||||
// Show flag emoji + text
|
||||
const flagEmoji = flagCode ? String.fromCodePoint(...[...flagCode.toUpperCase()].map(c => 127397 + c.charCodeAt())) : '';
|
||||
return $(`<span>${flagEmoji} ${state.text}</span>`);
|
||||
},
|
||||
width: '100%'
|
||||
width: '100%',
|
||||
// Enable search by country name or currency code
|
||||
matcher: function(params, data) {
|
||||
if ($.trim(params.term) === '') {
|
||||
return data;
|
||||
}
|
||||
const term = params.term.toLowerCase();
|
||||
const text = data.text.toLowerCase();
|
||||
const country = $(data.element).data('country');
|
||||
|
||||
if (text.indexOf(term) > -1 || (country && country.toLowerCase().indexOf(term) > -1)) {
|
||||
return data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
@props(['name' => 'timezone', 'id' => 'timezone', 'value' => '', 'required' => false, 'error' => null, 'label' => 'Timezone'])
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ $id }}" class="form-label">{{ $label }}</label>
|
||||
<div class="dropdown w-100" onclick="event.stopPropagation()">
|
||||
<button class="form-select dropdown-toggle d-flex align-items-center justify-content-between @error($name) is-invalid @enderror"
|
||||
type="button"
|
||||
id="{{ $id }}Dropdown"
|
||||
data-bs-toggle="dropdown"
|
||||
data-bs-auto-close="outside"
|
||||
aria-expanded="false"
|
||||
style="text-align: left; background-color: rgba(255,255,255,0.8);">
|
||||
<span class="d-flex align-items-center">
|
||||
<span id="{{ $id }}SelectedFlag"></span>
|
||||
<span class="timezone-label" id="{{ $id }}SelectedTimezone">Select Timezone</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="dropdown-menu p-2 w-100" aria-labelledby="{{ $id }}Dropdown" onclick="event.stopPropagation()">
|
||||
<input type="text"
|
||||
class="form-control form-control-sm mb-2"
|
||||
placeholder="Search timezone..."
|
||||
id="{{ $id }}Search"
|
||||
onmousedown="event.stopPropagation()"
|
||||
onfocus="event.stopPropagation()"
|
||||
oninput="event.stopPropagation()"
|
||||
onkeydown="event.stopPropagation()"
|
||||
onkeyup="event.stopPropagation()">
|
||||
|
||||
<div class="timezone-list" id="{{ $id }}List" data-component-id="{{ $id }}" style="max-height: 300px; overflow-y: auto;">
|
||||
<!-- Timezones will be populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="{{ $id }}" name="{{ $name }}" value="{{ $value }}" {{ $required ? 'required' : '' }}>
|
||||
</div>
|
||||
|
||||
@if($error)
|
||||
<span class="invalid-feedback d-block" role="alert">
|
||||
<strong>{{ $error }}</strong>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@once
|
||||
@push('styles')
|
||||
<style>
|
||||
.timezone-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.timezone-list .dropdown-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.timezone-list .dropdown-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Load countries from JSON file
|
||||
fetch('/data/countries.json')
|
||||
.then(response => response.json())
|
||||
.then(countries => {
|
||||
// Initialize all timezone dropdowns
|
||||
document.querySelectorAll('.timezone-list').forEach(function(listElement) {
|
||||
const componentId = listElement.getAttribute('data-component-id');
|
||||
initializeTimezoneDropdown(componentId, countries);
|
||||
});
|
||||
})
|
||||
.catch(error => console.error('Error loading countries:', error));
|
||||
|
||||
function initializeTimezoneDropdown(componentId, countries) {
|
||||
const timezoneList = document.getElementById(componentId + 'List');
|
||||
if (!timezoneList) return;
|
||||
|
||||
// Clear existing items
|
||||
timezoneList.innerHTML = '';
|
||||
|
||||
// Get unique timezones with their associated countries
|
||||
const timezoneMap = {};
|
||||
countries.forEach(country => {
|
||||
if (country.timezone && !timezoneMap[country.timezone]) {
|
||||
timezoneMap[country.timezone] = {
|
||||
timezone: country.timezone,
|
||||
flag: country.iso2,
|
||||
countryName: country.name
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Populate timezone dropdown
|
||||
Object.values(timezoneMap).forEach(tzData => {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'dropdown-item d-flex align-items-center';
|
||||
button.type = 'button';
|
||||
button.setAttribute('data-timezone', tzData.timezone);
|
||||
button.setAttribute('data-flag', tzData.flag);
|
||||
button.setAttribute('data-search', `${tzData.countryName.toLowerCase()} ${tzData.timezone.toLowerCase()}`);
|
||||
|
||||
// Convert flag code to emoji
|
||||
const flagEmoji = tzData.flag
|
||||
.toUpperCase()
|
||||
.split('')
|
||||
.map(char => String.fromCodePoint(127397 + char.charCodeAt(0)))
|
||||
.join('');
|
||||
|
||||
button.innerHTML = `
|
||||
<span class="me-2">${flagEmoji}</span>
|
||||
<span>${tzData.timezone}</span>
|
||||
`;
|
||||
button.addEventListener('click', function() {
|
||||
selectTimezone(componentId, tzData.timezone, flagEmoji);
|
||||
});
|
||||
timezoneList.appendChild(button);
|
||||
});
|
||||
|
||||
// Search functionality
|
||||
const searchInput = document.getElementById(componentId + 'Search');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function(e) {
|
||||
const searchTerm = e.target.value.toLowerCase();
|
||||
const items = timezoneList.querySelectorAll('.dropdown-item');
|
||||
items.forEach(item => {
|
||||
const searchText = item.getAttribute('data-search') || '';
|
||||
if (searchText.includes(searchTerm)) {
|
||||
item.classList.remove('d-none');
|
||||
} else {
|
||||
item.classList.add('d-none');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Set initial value if provided
|
||||
const hiddenInput = document.getElementById(componentId);
|
||||
if (hiddenInput && hiddenInput.value) {
|
||||
const initialTimezone = Object.values(timezoneMap).find(tz => tz.timezone === hiddenInput.value);
|
||||
if (initialTimezone) {
|
||||
const flagEmoji = initialTimezone.flag
|
||||
.toUpperCase()
|
||||
.split('')
|
||||
.map(char => String.fromCodePoint(127397 + char.charCodeAt(0)))
|
||||
.join('');
|
||||
selectTimezone(componentId, initialTimezone.timezone, flagEmoji);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectTimezone(componentId, timezone, flag) {
|
||||
const flagElement = document.getElementById(componentId + 'SelectedFlag');
|
||||
const timezoneElement = document.getElementById(componentId + 'SelectedTimezone');
|
||||
const hiddenInput = document.getElementById(componentId);
|
||||
|
||||
if (flagElement) flagElement.textContent = flag + ' ';
|
||||
if (timezoneElement) timezoneElement.textContent = timezone;
|
||||
if (hiddenInput) {
|
||||
hiddenInput.value = timezone;
|
||||
// Trigger change event for other handlers
|
||||
hiddenInput.dispatchEvent(new Event('change'));
|
||||
}
|
||||
|
||||
// Close the dropdown after selection
|
||||
const dropdownButton = document.getElementById(componentId + 'Dropdown');
|
||||
if (dropdownButton) {
|
||||
const dropdown = bootstrap.Dropdown.getInstance(dropdownButton);
|
||||
if (dropdown) dropdown.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Expose function globally for external use
|
||||
window.setTimezoneValue = function(componentId, timezone, countries) {
|
||||
const timezoneMap = {};
|
||||
countries.forEach(country => {
|
||||
if (country.timezone && !timezoneMap[country.timezone]) {
|
||||
timezoneMap[country.timezone] = {
|
||||
timezone: country.timezone,
|
||||
flag: country.iso2
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const tzData = timezoneMap[timezone];
|
||||
if (tzData) {
|
||||
const flagEmoji = tzData.flag
|
||||
.toUpperCase()
|
||||
.split('')
|
||||
.map(char => String.fromCodePoint(127397 + char.charCodeAt(0)))
|
||||
.join('');
|
||||
selectTimezone(componentId, timezone, flagEmoji);
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
@@ -41,12 +41,13 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Populate dropdown
|
||||
// Populate dropdown with country name and timezone
|
||||
Object.values(uniqueTimezones).forEach(timezoneData => {
|
||||
const option = document.createElement('option');
|
||||
option.value = timezoneData.timezone;
|
||||
option.textContent = `${timezoneData.name} (${timezoneData.timezone})`;
|
||||
option.textContent = `${timezoneData.timezone}`;
|
||||
option.setAttribute('data-flag', timezoneData.flag);
|
||||
option.setAttribute('data-country', timezoneData.name);
|
||||
selectElement.appendChild(option);
|
||||
});
|
||||
|
||||
@@ -55,7 +56,7 @@
|
||||
selectElement.value = initialValue;
|
||||
}
|
||||
|
||||
// Initialize Select2 for searchable dropdown
|
||||
// Initialize Select2 for searchable dropdown with flag emojis
|
||||
if (typeof $ !== 'undefined' && $.fn.select2) {
|
||||
$(selectElement).select2({
|
||||
templateResult: function(state) {
|
||||
@@ -64,7 +65,9 @@
|
||||
}
|
||||
const option = $(state.element);
|
||||
const flagCode = option.data('flag');
|
||||
return $(`<span><span class="fi fi-${flagCode} me-2"></span>${state.text}</span>`);
|
||||
// Convert ISO2 code to flag emoji
|
||||
const flagEmoji = flagCode ? String.fromCodePoint(...[...flagCode.toUpperCase()].map(c => 127397 + c.charCodeAt())) : '';
|
||||
return $(`<span>${flagEmoji} ${state.text}</span>`);
|
||||
},
|
||||
templateSelection: function(state) {
|
||||
if (!state.id) {
|
||||
@@ -72,7 +75,9 @@
|
||||
}
|
||||
const option = $(state.element);
|
||||
const flagCode = option.data('flag');
|
||||
return $(`<span><span class="fi fi-${flagCode} me-2"></span>${state.text}</span>`);
|
||||
// Convert ISO2 code to flag emoji
|
||||
const flagEmoji = flagCode ? String.fromCodePoint(...[...flagCode.toUpperCase()].map(c => 127397 + c.charCodeAt())) : '';
|
||||
return $(`<span>${flagEmoji} ${state.text}</span>`);
|
||||
},
|
||||
width: '100%'
|
||||
});
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
<!-- User Picker Modal -->
|
||||
<div class="modal fade" id="userPickerModal" tabindex="-1" aria-labelledby="userPickerModalLabel" aria-hidden="true" data-bs-backdrop="static">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content" style="border-radius: 1rem; border: none;">
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header border-0">
|
||||
<div>
|
||||
<h5 class="modal-title fw-bold" id="userPickerModalLabel">Select Club Owner</h5>
|
||||
<p class="text-muted small mb-0">Search and select a user to be the club owner</p>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body -->
|
||||
<div class="modal-body">
|
||||
<!-- Search Input -->
|
||||
<div class="mb-4">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-white">
|
||||
<i class="bi bi-search"></i>
|
||||
</span>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="userSearchInput"
|
||||
placeholder="Search by name, email, or phone..."
|
||||
autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div id="userPickerLoading" class="text-center py-5" style="display: none;">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="text-muted mt-2">Searching users...</p>
|
||||
</div>
|
||||
|
||||
<!-- Users List -->
|
||||
<div id="userPickerResults" style="max-height: 400px; overflow-y: auto;">
|
||||
<!-- Results will be populated here -->
|
||||
</div>
|
||||
|
||||
<!-- No Results -->
|
||||
<div id="userPickerNoResults" class="text-center py-5" style="display: none;">
|
||||
<i class="bi bi-person-x fs-1 text-muted mb-3"></i>
|
||||
<p class="text-muted mb-0">No users found</p>
|
||||
<small class="text-muted">Try a different search term</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script>
|
||||
(function() {
|
||||
const userPickerModal = document.getElementById('userPickerModal');
|
||||
if (!userPickerModal) return;
|
||||
|
||||
const searchInput = document.getElementById('userSearchInput');
|
||||
const resultsContainer = document.getElementById('userPickerResults');
|
||||
const loadingDiv = document.getElementById('userPickerLoading');
|
||||
const noResultsDiv = document.getElementById('userPickerNoResults');
|
||||
|
||||
let searchTimeout;
|
||||
let allUsers = [];
|
||||
|
||||
// Prevent club modal from closing when user picker opens
|
||||
userPickerModal.addEventListener('show.bs.modal', function() {
|
||||
const clubModal = document.getElementById('clubModal');
|
||||
if (clubModal) {
|
||||
clubModal.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
// Load all users when modal opens
|
||||
userPickerModal.addEventListener('shown.bs.modal', function() {
|
||||
searchInput.value = '';
|
||||
searchInput.focus();
|
||||
loadUsers();
|
||||
});
|
||||
|
||||
// Ensure club modal stays visible when user picker closes
|
||||
userPickerModal.addEventListener('hidden.bs.modal', function() {
|
||||
const clubModal = document.getElementById('clubModal');
|
||||
if (clubModal && clubModal.classList.contains('show')) {
|
||||
// Keep club modal visible
|
||||
document.body.classList.add('modal-open');
|
||||
const backdrop = document.querySelector('.modal-backdrop');
|
||||
if (backdrop) {
|
||||
backdrop.style.display = 'block';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Search with debounce
|
||||
searchInput.addEventListener('input', function() {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
filterUsers(this.value);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Load users from server
|
||||
async function loadUsers() {
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/api/users', {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
allUsers = await response.json();
|
||||
displayUsers(allUsers);
|
||||
} else {
|
||||
showError('Failed to load users');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading users:', error);
|
||||
showError('An error occurred while loading users');
|
||||
}
|
||||
}
|
||||
|
||||
// Filter users based on search term
|
||||
function filterUsers(searchTerm) {
|
||||
if (!searchTerm.trim()) {
|
||||
displayUsers(allUsers);
|
||||
return;
|
||||
}
|
||||
|
||||
const term = searchTerm.toLowerCase();
|
||||
const filtered = allUsers.filter(user => {
|
||||
return (
|
||||
user.full_name.toLowerCase().includes(term) ||
|
||||
user.email.toLowerCase().includes(term) ||
|
||||
(user.mobile && user.mobile.toLowerCase().includes(term))
|
||||
);
|
||||
});
|
||||
|
||||
displayUsers(filtered);
|
||||
}
|
||||
|
||||
// Display users in the list
|
||||
function displayUsers(users) {
|
||||
hideLoading();
|
||||
|
||||
if (users.length === 0) {
|
||||
resultsContainer.style.display = 'none';
|
||||
noResultsDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
resultsContainer.style.display = 'block';
|
||||
noResultsDiv.style.display = 'none';
|
||||
|
||||
resultsContainer.innerHTML = users.map(user => `
|
||||
<div class="user-card border rounded p-3 mb-2"
|
||||
style="cursor: pointer; transition: all 0.2s;"
|
||||
data-user-id="${user.id}"
|
||||
data-user-name="${user.full_name}"
|
||||
data-user-email="${user.email}"
|
||||
data-user-mobile="${user.mobile || ''}"
|
||||
data-user-picture="${user.profile_picture || ''}"
|
||||
onmouseover="this.style.backgroundColor='hsl(var(--muted) / 0.5)'; this.style.transform='translateX(4px)';"
|
||||
onmouseout="this.style.backgroundColor=''; this.style.transform='';">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
${user.profile_picture ? `
|
||||
<img src="${user.profile_picture}"
|
||||
alt="${user.full_name}"
|
||||
class="rounded-circle"
|
||||
style="width: 50px; height: 50px; object-fit: cover;">
|
||||
` : `
|
||||
<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center"
|
||||
style="width: 50px; height: 50px; font-size: 1.25rem; font-weight: 600;">
|
||||
${user.full_name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
`}
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-semibold">${user.full_name}</div>
|
||||
<div class="small text-muted">
|
||||
<i class="bi bi-envelope me-1"></i>${user.email}
|
||||
${user.mobile ? `<span class="ms-2"><i class="bi bi-phone me-1"></i>${user.mobile}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<i class="bi bi-check-circle text-primary" style="font-size: 1.5rem;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Attach click handlers
|
||||
resultsContainer.querySelectorAll('.user-card').forEach(card => {
|
||||
card.addEventListener('click', function() {
|
||||
selectUser({
|
||||
id: this.dataset.userId,
|
||||
name: this.dataset.userName,
|
||||
email: this.dataset.userEmail,
|
||||
mobile: this.dataset.userMobile,
|
||||
picture: this.dataset.userPicture
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Select a user
|
||||
function selectUser(user) {
|
||||
// Update hidden input
|
||||
const ownerInput = document.getElementById('owner_user_id');
|
||||
if (ownerInput) {
|
||||
ownerInput.value = user.id;
|
||||
ownerInput.dispatchEvent(new Event('change'));
|
||||
}
|
||||
|
||||
// Update display
|
||||
const ownerDisplay = document.getElementById('ownerDisplay');
|
||||
if (ownerDisplay) {
|
||||
ownerDisplay.innerHTML = `
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
${user.picture ? `
|
||||
<img src="${user.picture}"
|
||||
alt="${user.name}"
|
||||
class="rounded-circle"
|
||||
style="width: 50px; height: 50px; object-fit: cover;">
|
||||
` : `
|
||||
<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center"
|
||||
style="width: 50px; height: 50px; font-size: 1.25rem; font-weight: 600;">
|
||||
${user.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
`}
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-semibold">${user.name}</div>
|
||||
<div class="small text-muted">
|
||||
<i class="bi bi-envelope me-1"></i>${user.email}
|
||||
${user.mobile ? `<span class="ms-2"><i class="bi bi-phone me-1"></i>${user.mobile}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Close modal
|
||||
bootstrap.Modal.getInstance(userPickerModal).hide();
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
function showLoading() {
|
||||
loadingDiv.style.display = 'block';
|
||||
resultsContainer.style.display = 'none';
|
||||
noResultsDiv.style.display = 'none';
|
||||
}
|
||||
|
||||
// Hide loading state
|
||||
function hideLoading() {
|
||||
loadingDiv.style.display = 'none';
|
||||
}
|
||||
|
||||
// Show error
|
||||
function showError(message) {
|
||||
hideLoading();
|
||||
resultsContainer.innerHTML = `
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>${message}
|
||||
</div>
|
||||
`;
|
||||
resultsContainer.style.display = 'block';
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
Reference in new issue
Block a user