add appointment

This commit is contained in:
Nyavokevin 2025-09-10 12:10:37 +03:00
parent 39c17b44cf
commit ae9a4799be
10 changed files with 422 additions and 6 deletions

View 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()
]);
}
}
}

View File

@ -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');

View File

@ -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);

View File

@ -21,7 +21,8 @@ class Payment extends Model
'client_session_id',
'draw_count',
'status',
'cards'
'cards',
'appointment_date'
];
/**

View File

@ -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');
});
}
};

View 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>

View 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>

View 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>

View File

@ -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');