| 1 | import {
|
|---|
| 2 | type Component,
|
|---|
| 3 | createEffect,
|
|---|
| 4 | createResource,
|
|---|
| 5 | createSignal,
|
|---|
| 6 | For,
|
|---|
| 7 | Show,
|
|---|
| 8 | } from "solid-js";
|
|---|
| 9 | import { useNavigate } from "@solidjs/router";
|
|---|
| 10 | import { useAuth } from "@/context/AuthContext";
|
|---|
| 11 | import { consultationSlotApi } from "@/api/consultationSlot";
|
|---|
| 12 | import { formatDateWithWeekday, getTodayString } from "@/utils";
|
|---|
| 13 | import { UserType } from "@/enums/UserType";
|
|---|
| 14 |
|
|---|
| 15 | const ConsultationSlots: Component = () => {
|
|---|
| 16 | const { user, isAuthenticated } = useAuth();
|
|---|
| 17 | const navigate = useNavigate();
|
|---|
| 18 | const [selectedDate, setSelectedDate] = createSignal("");
|
|---|
| 19 | const [error, setError] = createSignal("");
|
|---|
| 20 |
|
|---|
| 21 | const [slots, { refetch }] = createResource(
|
|---|
| 22 | () => ({
|
|---|
| 23 | authenticated: isAuthenticated(),
|
|---|
| 24 | userId: user()?.userId,
|
|---|
| 25 | }),
|
|---|
| 26 |
|
|---|
| 27 | async (params) => {
|
|---|
| 28 | if (!params.authenticated || !params.userId) return null;
|
|---|
| 29 | return await consultationSlotApi.getSlots(params.userId);
|
|---|
| 30 | },
|
|---|
| 31 | );
|
|---|
| 32 |
|
|---|
| 33 | createEffect(() => {
|
|---|
| 34 | if (!isAuthenticated()) {
|
|---|
| 35 | navigate("/login", { replace: true });
|
|---|
| 36 | return;
|
|---|
| 37 | }
|
|---|
| 38 |
|
|---|
| 39 | const currentUser = user();
|
|---|
| 40 | if (currentUser?.userType !== UserType.THERAPIST) {
|
|---|
| 41 | navigate("/", { replace: true });
|
|---|
| 42 | }
|
|---|
| 43 | });
|
|---|
| 44 |
|
|---|
| 45 | const handleAddSlot = async (e: Event) => {
|
|---|
| 46 | e.preventDefault();
|
|---|
| 47 | const dateStr = selectedDate();
|
|---|
| 48 |
|
|---|
| 49 | if (!dateStr) {
|
|---|
| 50 | setError("Please select a date");
|
|---|
| 51 | return;
|
|---|
| 52 | }
|
|---|
| 53 |
|
|---|
| 54 | if (dateStr < getTodayString()) {
|
|---|
| 55 | setError("Cannot add slots for past dates");
|
|---|
| 56 | return;
|
|---|
| 57 | }
|
|---|
| 58 |
|
|---|
| 59 | const currentUser = user();
|
|---|
| 60 | if (!currentUser?.userId) return;
|
|---|
| 61 |
|
|---|
| 62 | try {
|
|---|
| 63 | await consultationSlotApi.addSlot(currentUser.userId, dateStr);
|
|---|
| 64 | setSelectedDate("");
|
|---|
| 65 | setError("");
|
|---|
| 66 | refetch();
|
|---|
| 67 | } catch (err: any) {
|
|---|
| 68 | setError(err.message || "Failed to add slot");
|
|---|
| 69 | }
|
|---|
| 70 | };
|
|---|
| 71 |
|
|---|
| 72 | const handleRemoveSlot = async (date: string) => {
|
|---|
| 73 | const currentUser = user();
|
|---|
| 74 | if (!currentUser?.userId) return;
|
|---|
| 75 |
|
|---|
| 76 | if (
|
|---|
| 77 | !confirm(`Remove consultation slot for ${formatDateWithWeekday(date)}?`)
|
|---|
| 78 | ) {
|
|---|
| 79 | return;
|
|---|
| 80 | }
|
|---|
| 81 |
|
|---|
| 82 | try {
|
|---|
| 83 | await consultationSlotApi.removeSlot(currentUser.userId, date);
|
|---|
| 84 | setError("");
|
|---|
| 85 | refetch();
|
|---|
| 86 | } catch (err: any) {
|
|---|
| 87 | setError(err.message || "Failed to remove slot");
|
|---|
| 88 | }
|
|---|
| 89 | };
|
|---|
| 90 |
|
|---|
| 91 | const sortedSlots = () => {
|
|---|
| 92 | const slotData = slots();
|
|---|
| 93 | if (!slotData?.consultationSlots) return [];
|
|---|
| 94 | return [...slotData.consultationSlots].sort();
|
|---|
| 95 | };
|
|---|
| 96 |
|
|---|
| 97 | const futureSlots = () => {
|
|---|
| 98 | const today = getTodayString();
|
|---|
| 99 | return sortedSlots().filter((slot) => slot >= today);
|
|---|
| 100 | };
|
|---|
| 101 |
|
|---|
| 102 | const pastSlots = () => {
|
|---|
| 103 | const today = getTodayString();
|
|---|
| 104 | return sortedSlots().filter((slot) => slot < today);
|
|---|
| 105 | };
|
|---|
| 106 |
|
|---|
| 107 | return (
|
|---|
| 108 | <div class="container mx-auto px-4 py-8">
|
|---|
| 109 | <div class="mb-8">
|
|---|
| 110 | <h1 class="text-3xl font-bold text-gray-900 mb-2">
|
|---|
| 111 | Manage Consultation Slots
|
|---|
| 112 | </h1>
|
|---|
| 113 | <p class="text-gray-600">
|
|---|
| 114 | Add or remove dates when you're available for patient consultations
|
|---|
| 115 | </p>
|
|---|
| 116 | </div>
|
|---|
| 117 |
|
|---|
| 118 | <Show
|
|---|
| 119 | when={!slots.loading}
|
|---|
| 120 | fallback={
|
|---|
| 121 | <div class="flex justify-center items-center py-12">
|
|---|
| 122 | <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
|---|
| 123 | </div>
|
|---|
| 124 | }
|
|---|
| 125 | >
|
|---|
| 126 | <div class="grid gap-6 lg:grid-cols-2">
|
|---|
| 127 | <div class="bg-white rounded-lg shadow-md p-6">
|
|---|
| 128 | <h2 class="text-xl font-bold text-gray-900 mb-4">
|
|---|
| 129 | Add New Consultation Slot
|
|---|
| 130 | </h2>
|
|---|
| 131 | <form onSubmit={handleAddSlot} class="space-y-4">
|
|---|
| 132 | <div>
|
|---|
| 133 | <label class="block text-sm font-semibold text-gray-700 mb-2">
|
|---|
| 134 | Select Date
|
|---|
| 135 | </label>
|
|---|
| 136 | <input
|
|---|
| 137 | type="date"
|
|---|
| 138 | value={selectedDate()}
|
|---|
| 139 | onInput={(e) => setSelectedDate(e.currentTarget.value)}
|
|---|
| 140 | min={getTodayString()}
|
|---|
| 141 | class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|---|
| 142 | required
|
|---|
| 143 | />
|
|---|
| 144 | </div>
|
|---|
| 145 |
|
|---|
| 146 | <Show when={error()}>
|
|---|
| 147 | <div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg text-sm">
|
|---|
| 148 | {error()}
|
|---|
| 149 | </div>
|
|---|
| 150 | </Show>
|
|---|
| 151 |
|
|---|
| 152 | <button
|
|---|
| 153 | type="submit"
|
|---|
| 154 | class="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-semibold cursor-pointer"
|
|---|
| 155 | >
|
|---|
| 156 | Add Slot
|
|---|
| 157 | </button>
|
|---|
| 158 | </form>
|
|---|
| 159 | </div>
|
|---|
| 160 |
|
|---|
| 161 | <div class="bg-white rounded-lg shadow-md p-6">
|
|---|
| 162 | <h2 class="text-xl font-bold text-gray-900 mb-4">
|
|---|
| 163 | Upcoming Consultation Slots ({futureSlots().length})
|
|---|
| 164 | </h2>
|
|---|
| 165 | <Show
|
|---|
| 166 | when={futureSlots().length > 0}
|
|---|
| 167 | fallback={
|
|---|
| 168 | <p class="text-gray-500 text-sm italic">
|
|---|
| 169 | No upcoming consultation slots scheduled
|
|---|
| 170 | </p>
|
|---|
| 171 | }
|
|---|
| 172 | >
|
|---|
| 173 | <div class="space-y-2 max-h-96 overflow-y-auto">
|
|---|
| 174 | <For each={futureSlots()}>
|
|---|
| 175 | {(slot) => (
|
|---|
| 176 | <div class="flex items-center justify-between p-3 bg-green-50 border border-green-200 rounded-lg">
|
|---|
| 177 | <div class="flex items-center gap-3">
|
|---|
| 178 | <svg
|
|---|
| 179 | class="w-5 h-5 text-green-600"
|
|---|
| 180 | fill="none"
|
|---|
| 181 | stroke="currentColor"
|
|---|
| 182 | viewBox="0 0 24 24"
|
|---|
| 183 | >
|
|---|
| 184 | <path
|
|---|
| 185 | stroke-linecap="round"
|
|---|
| 186 | stroke-linejoin="round"
|
|---|
| 187 | stroke-width="2"
|
|---|
| 188 | d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|---|
| 189 | />
|
|---|
| 190 | </svg>
|
|---|
| 191 | <span class="text-sm font-medium text-gray-900">
|
|---|
| 192 | {formatDateWithWeekday(slot)}
|
|---|
| 193 | </span>
|
|---|
| 194 | </div>
|
|---|
| 195 | <button
|
|---|
| 196 | onClick={() => handleRemoveSlot(slot)}
|
|---|
| 197 | class="px-3 py-1 text-red-700 bg-red-50 border border-red-200 rounded hover:bg-red-100 transition-colors text-sm font-medium cursor-pointer"
|
|---|
| 198 | >
|
|---|
| 199 | Remove
|
|---|
| 200 | </button>
|
|---|
| 201 | </div>
|
|---|
| 202 | )}
|
|---|
| 203 | </For>
|
|---|
| 204 | </div>
|
|---|
| 205 | </Show>
|
|---|
| 206 | </div>
|
|---|
| 207 | </div>
|
|---|
| 208 |
|
|---|
| 209 | <Show when={pastSlots().length > 0}>
|
|---|
| 210 | <div class="mt-6 bg-gray-50 rounded-lg shadow-md p-6">
|
|---|
| 211 | <h2 class="text-xl font-bold text-gray-700 mb-4">
|
|---|
| 212 | Past Consultation Slots ({pastSlots().length})
|
|---|
| 213 | </h2>
|
|---|
| 214 | <div class="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
|---|
| 215 | <For each={pastSlots()}>
|
|---|
| 216 | {(slot) => (
|
|---|
| 217 | <div class="flex items-center gap-2 p-2 bg-gray-100 border border-gray-300 rounded text-gray-600">
|
|---|
| 218 | <svg
|
|---|
| 219 | class="w-4 h-4"
|
|---|
| 220 | fill="none"
|
|---|
| 221 | stroke="currentColor"
|
|---|
| 222 | viewBox="0 0 24 24"
|
|---|
| 223 | >
|
|---|
| 224 | <path
|
|---|
| 225 | stroke-linecap="round"
|
|---|
| 226 | stroke-linejoin="round"
|
|---|
| 227 | stroke-width="2"
|
|---|
| 228 | d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
|---|
| 229 | />
|
|---|
| 230 | </svg>
|
|---|
| 231 | <span class="text-xs">{formatDateWithWeekday(slot)}</span>
|
|---|
| 232 | </div>
|
|---|
| 233 | )}
|
|---|
| 234 | </For>
|
|---|
| 235 | </div>
|
|---|
| 236 | </div>
|
|---|
| 237 | </Show>
|
|---|
| 238 | </Show>
|
|---|
| 239 | </div>
|
|---|
| 240 | );
|
|---|
| 241 | };
|
|---|
| 242 |
|
|---|
| 243 | export default ConsultationSlots;
|
|---|