add appointment
This commit is contained in:
parent
39c17b44cf
commit
ae9a4799be
20
app/Http/Controllers/AppointmentController.php
Normal file
20
app/Http/Controllers/AppointmentController.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class AppointmentController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
try{
|
||||
return Inertia::render('Agenda');
|
||||
}catch (\Exception $e) {
|
||||
Log::error('Error fetching agenda: '.$e->getMessage(), [
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -69,6 +69,49 @@ class StripeController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
public function createRendezVousSession(Request $request)
|
||||
{
|
||||
Stripe::setApiKey(env('STRIPE_SECRET_KEY'));
|
||||
|
||||
$userForm = $request->input('userForm');
|
||||
$dateAppointment = $request->input('selectedDate');
|
||||
$clientSessionId = Str::uuid();
|
||||
$priceId = 'price_1S5ifuGaZ3yeYkzWsgrOTpgT';
|
||||
|
||||
try {
|
||||
$session = Session::create([
|
||||
'line_items' => [[
|
||||
'price' => $priceId,
|
||||
'quantity' => 1,
|
||||
]],
|
||||
'mode' => 'payment',
|
||||
'success_url' => url(env('APP_URL') . '/rendez-vous/success?client_session_id=' . $clientSessionId),
|
||||
'cancel_url' => url(env('APP_URL') . '/cancel'),
|
||||
'metadata' => [
|
||||
'client_session_id' => $clientSessionId,
|
||||
'type_appointment' => true,
|
||||
'appointment_date' => $dateAppointment
|
||||
],
|
||||
'customer_email' => $userForm["email"]
|
||||
]);
|
||||
|
||||
Payment::create([
|
||||
'amount' => $session->amount_total / 100,
|
||||
'currency' => $session->currency,
|
||||
'stripe_session_id' => $session->id,
|
||||
'client_session_id' => $clientSessionId,
|
||||
'draw_count' => 0,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
return response()->json(['sessionId' => $session->id]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Stripe session creation failed: ' . $e->getMessage());
|
||||
return response()->json(['error' => 'Could not create checkout session.'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function validatePayment(Request $request)
|
||||
{
|
||||
$clientSessionId = $request->query('client_session_id');
|
||||
|
||||
@ -8,6 +8,7 @@ use Stripe\Stripe;
|
||||
use Stripe\Webhook;
|
||||
use Stripe\Exception\SignatureVerificationException;
|
||||
use Log; // Import the Log facade
|
||||
use DateTime;
|
||||
|
||||
class WebhookController extends Controller
|
||||
{
|
||||
@ -31,16 +32,25 @@ class WebhookController extends Controller
|
||||
switch ($event->type) {
|
||||
case 'checkout.session.completed':
|
||||
$session = $event->data->object;
|
||||
$drawCount = $session->metadata->draw_count;
|
||||
$clientSessionId = $session->metadata->client_session_id;
|
||||
|
||||
$payment = Payment::where('client_session_id', $clientSessionId)->first();
|
||||
|
||||
if ($payment) {
|
||||
$payment->update([
|
||||
'status' => 'succeeded',
|
||||
'draw_count' => $drawCount,
|
||||
]);
|
||||
if (isset($session->metadata->type_appointment) && $session->metadata->type_appointment === 'true') {
|
||||
$dateTimeObj = new DateTime($session->metadata->appointment_date);
|
||||
$payment->update([
|
||||
'status' => 'succeeded',
|
||||
'appointment_date' => $dateTimeObj->format('Y-m-d')
|
||||
]);
|
||||
} else {
|
||||
// Original logic for other payments
|
||||
$drawCount = $session->metadata->draw_count;
|
||||
$payment->update([
|
||||
'status' => 'succeeded',
|
||||
'draw_count' => $drawCount,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
// Log if no matching payment record is found
|
||||
Log::warning('No pending payment record found for client_session_id: ' . $clientSessionId);
|
||||
|
||||
@ -21,7 +21,8 @@ class Payment extends Model
|
||||
'client_session_id',
|
||||
'draw_count',
|
||||
'status',
|
||||
'cards'
|
||||
'cards',
|
||||
'appointment_date'
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('payments', function (Blueprint $table) {
|
||||
$table->date('appointment_date')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('payments', function (Blueprint $table) {
|
||||
$table->dropColumn('appointment_date');
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
122
resources/js/components/DatePicker.vue
Normal file
122
resources/js/components/DatePicker.vue
Normal file
@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="flex max-w-md min-w-[320px] flex-1 flex-col gap-6">
|
||||
<p class="text-center text-lg text-[var(--midnight-blue)]">Sélectionnez une date disponible</p>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<button @click="handlePreviousMonth" class="rounded-full p-2 text-[var(--midnight-blue)] hover:bg-[var(--linen)]">
|
||||
<svg fill="currentColor" height="24px" viewBox="0 0 256 256" width="24px" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M165.66,202.34a8,8,0,0,1-11.32,11.32l-80-80a8,8,0,0,1,0-11.32l80-80a8,8,0,0,1,11.32,11.32L91.31,128Z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<h3 class="text-center text-2xl font-bold text-[var(--midnight-blue)]">{{ monthName }} {{ currentYear }}</h3>
|
||||
<button @click="handleNextMonth" class="rounded-full p-2 text-[var(--midnight-blue)] hover:bg-[var(--linen)]">
|
||||
<svg fill="currentColor" height="24px" viewBox="0 0 256 256" width="24px" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-7 gap-1">
|
||||
<p v-for="day in dayNames" :key="day" class="p-3 text-center text-sm font-bold text-[var(--spiritual-earth)]">{{ day }}</p>
|
||||
|
||||
<div v-for="blank in firstDayOfMonth" :key="'blank-' + blank" class="col-start-auto"></div>
|
||||
|
||||
<button
|
||||
v-for="day in daysInMonth"
|
||||
:key="day"
|
||||
@click="selectDay(day)"
|
||||
:class="{
|
||||
'rounded-full p-3 text-base font-medium hover:bg-[var(--linen)]': true,
|
||||
'bg-[var(--spiritual-earth)] text-[var(--pure-white)] ring-2 ring-[var(--subtle-gold)] ring-offset-2 ring-offset-[var(--light-ivory)]':
|
||||
isSelected(day),
|
||||
'text-gray-400': isDisabled(day),
|
||||
}"
|
||||
:disabled="isDisabled(day)"
|
||||
>
|
||||
{{ day }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineEmits, defineProps, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
selectedDate: {
|
||||
type: Date,
|
||||
default: () => new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:selectedDate']);
|
||||
|
||||
const today = new Date();
|
||||
const currentMonth = ref(props.selectedDate.getMonth());
|
||||
const currentYear = ref(props.selectedDate.getFullYear());
|
||||
|
||||
const dayNames = ['D', 'L', 'M', 'M', 'J', 'V', 'S'];
|
||||
const monthNames = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
|
||||
|
||||
const monthName = computed(() => {
|
||||
return monthNames[currentMonth.value];
|
||||
});
|
||||
|
||||
const firstDayOfMonth = computed(() => {
|
||||
const date = new Date(currentYear.value, currentMonth.value, 1);
|
||||
return date.getDay(); // 0 for Sunday, 1 for Monday...
|
||||
});
|
||||
|
||||
const daysInMonth = computed(() => {
|
||||
return new Date(currentYear.value, currentMonth.value + 1, 0).getDate();
|
||||
});
|
||||
|
||||
const handlePreviousMonth = () => {
|
||||
if (currentMonth.value === 0) {
|
||||
currentMonth.value = 11;
|
||||
currentYear.value--;
|
||||
} else {
|
||||
currentMonth.value--;
|
||||
}
|
||||
};
|
||||
|
||||
const handleNextMonth = () => {
|
||||
if (currentMonth.value === 11) {
|
||||
currentMonth.value = 0;
|
||||
currentYear.value++;
|
||||
} else {
|
||||
currentMonth.value++;
|
||||
}
|
||||
};
|
||||
|
||||
const selectDay = (day: number) => {
|
||||
const newDate = new Date(currentYear.value, currentMonth.value, day);
|
||||
emit('update:selectedDate', newDate);
|
||||
};
|
||||
|
||||
const isSelected = (day: number) => {
|
||||
return (
|
||||
day === props.selectedDate.getDate() &&
|
||||
currentMonth.value === props.selectedDate.getMonth() &&
|
||||
currentYear.value === props.selectedDate.getFullYear()
|
||||
);
|
||||
};
|
||||
|
||||
const isDisabled = (day: number) => {
|
||||
const date = new Date(currentYear.value, currentMonth.value, day);
|
||||
const todayAtMidnight = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
return date.getTime() < todayAtMidnight.getTime();
|
||||
};
|
||||
|
||||
// Watch for changes in the prop to update internal state
|
||||
watch(
|
||||
() => props.selectedDate,
|
||||
(newDate) => {
|
||||
currentMonth.value = newDate.getMonth();
|
||||
currentYear.value = newDate.getFullYear();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
94
resources/js/pages/Agenda.vue
Normal file
94
resources/js/pages/Agenda.vue
Normal file
@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
import DatePicker from '@/components/DatePicker.vue';
|
||||
import LandingLayout from '@/layouts/app/LandingLayout.vue';
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
import axios from 'axios';
|
||||
import { ref } from 'vue';
|
||||
|
||||
interface UserForm {
|
||||
fullname: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
const userForm = ref<UserForm>({
|
||||
fullname: '',
|
||||
email: '',
|
||||
});
|
||||
const selectedDate = ref(new Date());
|
||||
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
const handleAppointment = () => {
|
||||
redirectToStipeCheckout();
|
||||
};
|
||||
|
||||
const redirectToStipeCheckout = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await axios.post('/checkout-rendez-vous', {
|
||||
userForm: userForm.value,
|
||||
selectedDate: selectedDate.value,
|
||||
});
|
||||
const sessionId = res.data.sessionId;
|
||||
console.log(sessionId);
|
||||
const stripe = await loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY!);
|
||||
if (stripe) {
|
||||
const { error } = await stripe.redirectToCheckout({ sessionId });
|
||||
|
||||
if (error) {
|
||||
console.error('Stripe redirect error:', error.message);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initiating Stripe checkout:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LandingLayout>
|
||||
<main class="flex flex-1 justify-center px-10 py-16">
|
||||
<div class="layout-content-container flex w-full max-w-4xl flex-col items-center gap-12">
|
||||
<h1 class="text-center text-5xl font-bold text-[var(--midnight-blue)]">Réservez votre consultation</h1>
|
||||
<div class="flex w-full flex-wrap items-start justify-center gap-16">
|
||||
<date-picker v-model:selectedDate="selectedDate" />
|
||||
<div class="flex max-w-md min-w-[320px] flex-1 flex-col gap-6">
|
||||
<p class="text-center text-lg text-[var(--midnight-blue)]">Entrez vos informations</p>
|
||||
<form class="flex flex-col gap-6" @submit.prevent="handleAppointment">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-[var(--midnight-blue)]" for="name">Nom complet</label>
|
||||
<input
|
||||
class="form-input w-full rounded-lg border-[var(--linen)] bg-[var(--pure-white)] p-3 text-base text-[var(--midnight-blue)] focus:border-[var(--subtle-gold)] focus:ring-[var(--subtle-gold)]"
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder="Votre nom complet"
|
||||
type="text"
|
||||
v-model="userForm.fullname"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-[var(--midnight-blue)]" for="email">Adresse e-mail</label>
|
||||
<input
|
||||
class="form-input w-full rounded-lg border-[var(--linen)] bg-[var(--pure-white)] p-3 text-base text-[var(--midnight-blue)] focus:border-[var(--subtle-gold)] focus:ring-[var(--subtle-gold)]"
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="Votre adresse e-mail"
|
||||
type="email"
|
||||
v-model="userForm.email"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="mt-6 flex h-14 w-full cursor-pointer items-center justify-center rounded-full bg-[var(--midnight-blue)] px-8 text-lg font-bold tracking-wide text-[var(--pure-white)] shadow-[var(--linen)] shadow-lg transition-colors duration-300 hover:bg-[var(--spiritual-earth)]"
|
||||
type="submit"
|
||||
>
|
||||
<span class="truncate">Confirmer et Payer</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</LandingLayout>
|
||||
</template>
|
||||
79
resources/js/pages/AppointSuccess.vue
Normal file
79
resources/js/pages/AppointSuccess.vue
Normal file
@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<LandingLayout>
|
||||
<main class="flex flex-grow items-center justify-center">
|
||||
<div class="w-full max-w-2xl px-4 py-20 sm:px-6 lg:px-8">
|
||||
<div class="flex flex-col items-center rounded-2xl border border-[var(--linen)] bg-[var(--pure-white)] p-12 text-center shadow-xl">
|
||||
<div class="mb-8 flex h-20 w-20 items-center justify-center rounded-full bg-[var(--subtle-gold)] shadow-lg">
|
||||
<svg
|
||||
class="h-10 w-10 text-[var(--midnight-blue)]"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 256 256"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="text-4xl font-bold text-[var(--midnight-blue)] md:text-5xl">Paiement Réussi</h1>
|
||||
<p class="mx-auto mt-4 max-w-md text-lg text-[var(--midnight-blue)]/80">
|
||||
Votre rendez-vous a été confirmé ! L'univers vous attend.
|
||||
</p>
|
||||
<div class="mt-10 w-full border-t border-[var(--linen)]"></div>
|
||||
<p class="mt-10 text-lg font-medium text-[var(--midnight-blue)]/90">
|
||||
Votre rendez-vous est programmé. Veuillez l'ajouter à votre calendrier.
|
||||
</p>
|
||||
|
||||
<a
|
||||
:href="googleCalendarUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="mt-8 flex h-12 max-w-[480px] min-w-[200px] cursor-pointer items-center justify-center overflow-hidden rounded-full bg-[var(--midnight-blue)] px-8 text-base font-bold tracking-wide text-[var(--pure-white)] transition-all duration-300 hover:bg-[var(--spiritual-earth)] hover:shadow-[var(--spiritual-earth)]/30 hover:shadow-lg"
|
||||
>
|
||||
<span class="truncate">Ajouter à Google Calendar</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</LandingLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LandingLayout from '@/layouts/app/LandingLayout.vue';
|
||||
import { usePage } from '@inertiajs/vue3';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const page = usePage();
|
||||
|
||||
const appointmentDate = computed(() => page.props.appointment_date);
|
||||
const appointmentTime = computed(() => page.props.appointment_time); // Assumes you're also passing the time
|
||||
|
||||
const loading = ref(true);
|
||||
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
}, 2000);
|
||||
|
||||
const googleCalendarUrl = computed(() => {
|
||||
if (!appointmentDate.value) return '#';
|
||||
|
||||
// Combine date and time to create a full datetime object
|
||||
const startDateTimeString = `${appointmentDate.value}T${appointmentTime.value || '09:00:00'}`;
|
||||
const startDate = new Date(startDateTimeString);
|
||||
const endDate = new Date(startDate.getTime() + 60 * 60 * 1000); // Assumes a 1-hour appointment
|
||||
|
||||
// Format for Google Calendar URL
|
||||
const formatGoogleDate = (date: Date) => {
|
||||
return date.toISOString().replace(/[-:]/g, '').split('.')[0];
|
||||
};
|
||||
|
||||
const start = formatGoogleDate(startDate);
|
||||
const end = formatGoogleDate(endDate);
|
||||
|
||||
const title = encodeURIComponent('Consultation de Tarot avec [Nom du site]');
|
||||
const description = encodeURIComponent('Votre consultation de tarot est confirmée. Préparez vos questions !');
|
||||
const location = encodeURIComponent('En ligne via [Lien de la visioconférence]');
|
||||
|
||||
return `https://www.google.com/calendar/render?action=TEMPLATE&text=${title}&dates=${start}/${end}&details=${description}&location=${location}`;
|
||||
});
|
||||
</script>
|
||||
@ -19,8 +19,12 @@ Route::post('/draw-card', [App\Http\Controllers\CardController::class, 'drawCard
|
||||
|
||||
Route::post('/create-checkout-session', [App\Http\Controllers\StripeController::class, 'createCheckoutSession']);
|
||||
|
||||
Route::post('/checkout-rendez-vous', [App\Http\Controllers\StripeController::class, 'createRendezVousSession']);
|
||||
|
||||
Route::post('/stripe/webhook', [App\Http\Controllers\WebhookController::class, 'handleWebhook']);
|
||||
|
||||
Route::get('/rendez-vous', [App\Http\Controllers\AppointmentController::class, 'index']);
|
||||
|
||||
Route::get('/success', function (Request $request) {
|
||||
$clientSessionId = $request->query('client_session_id');
|
||||
|
||||
@ -38,6 +42,20 @@ Route::get('/success', function (Request $request) {
|
||||
return Inertia::render('payments/Error', ['message' => 'Payment validation failed.']);
|
||||
})->name('payment.success');
|
||||
|
||||
Route::get('/rendez-vous/success', function (Request $request){
|
||||
$clientSessionId = $request->query('client_session_id');
|
||||
$payment = Payment::where('client_session_id', $clientSessionId)
|
||||
->where('status', 'succeeded') // Only check for succeeded payments
|
||||
->first();
|
||||
|
||||
if ($payment) {
|
||||
return Inertia::render('AppointSuccess', [
|
||||
'appointment_date' => $payment->appointment_date,
|
||||
]);
|
||||
}
|
||||
|
||||
})->name('appoint.success');
|
||||
|
||||
Route::get('/cancel', function () {
|
||||
return Inertia::render('payments/Error'); // Your error page
|
||||
})->name('payments.cancel');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user