|
13 | 13 | const { data: clients } = await useFetch('/api/get/clients') |
14 | 14 |
|
15 | 15 | const search = ref('') |
16 | | - const activeFilters = ref<{ label: string; value: string }[]>([]) |
17 | | - const excludedFilters = ref<{ label: string; value: string }[]>([]) |
| 16 | +
|
| 17 | + // Persisted State |
| 18 | + const savedActiveFilters = useCookie<{ label: string; value: string }[]>('ride-active-filters', { |
| 19 | + default: () => [], |
| 20 | + }) |
| 21 | + const savedExcludedFilters = useCookie<{ label: string; value: string }[]>( |
| 22 | + 'ride-excluded-filters', |
| 23 | + { default: () => [] } |
| 24 | + ) |
| 25 | +
|
| 26 | + const activeFilters = ref<{ label: string; value: string }[]>(savedActiveFilters.value) |
| 27 | + const excludedFilters = ref<{ label: string; value: string }[]>(savedExcludedFilters.value) |
| 28 | +
|
| 29 | + // Sync state back to cookies |
| 30 | + watch( |
| 31 | + activeFilters, |
| 32 | + (newVal) => { |
| 33 | + savedActiveFilters.value = newVal |
| 34 | + }, |
| 35 | + { deep: true } |
| 36 | + ) |
| 37 | + watch( |
| 38 | + excludedFilters, |
| 39 | + (newVal) => { |
| 40 | + savedExcludedFilters.value = newVal |
| 41 | + }, |
| 42 | + { deep: true } |
| 43 | + ) |
| 44 | +
|
18 | 45 | const startDate = ref('') |
19 | 46 | const endDate = ref('') |
20 | 47 | const isCreateModalOpen = ref(false) |
|
23 | 50 | const options = [ |
24 | 51 | { label: 'Created', value: 'status:CREATED' }, |
25 | 52 | { label: 'Assigned', value: 'status:ASSIGNED' }, |
26 | | - { label: 'Completed', value: 'status:COMPLETED' } |
| 53 | + { label: 'Completed', value: 'status:COMPLETED' }, |
27 | 54 | ] |
28 | 55 | if (!isAdmin.value) { |
29 | 56 | options.push({ label: 'Assigned to Me', value: 'assign:ME' }) |
|
41 | 68 | // Consolidated Filter Logic (OR Condition for Inclusion) |
42 | 69 | if (activeFilters.value.length > 0) { |
43 | 70 | result = result.filter((ride: any) => { |
44 | | - return activeFilters.value.some(filter => { |
| 71 | + return activeFilters.value.some((filter) => { |
45 | 72 | const val = filter.value |
46 | | - |
| 73 | +
|
47 | 74 | if (val.startsWith('status:')) { |
48 | 75 | const status = val.replace('status:', '') |
49 | 76 | return ride.status === status |
50 | 77 | } |
51 | | - |
| 78 | +
|
52 | 79 | if (val === 'assign:ME') { |
53 | 80 | const myId = session.value?.user?.id |
54 | 81 | return !!(myId && ride.volunteer?.userId === myId) |
55 | 82 | } |
56 | | - |
| 83 | +
|
57 | 84 | return false |
58 | 85 | }) |
59 | 86 | }) |
|
63 | 90 | if (excludedFilters.value.length > 0) { |
64 | 91 | result = result.filter((ride: any) => { |
65 | 92 | // Must NOT match ANY of the excluded filters |
66 | | - return !excludedFilters.value.some(filter => { |
| 93 | + return !excludedFilters.value.some((filter) => { |
67 | 94 | const val = filter.value |
68 | | - |
| 95 | +
|
69 | 96 | if (val.startsWith('status:')) { |
70 | 97 | const status = val.replace('status:', '') |
71 | 98 | return ride.status === status |
72 | 99 | } |
73 | | - |
| 100 | +
|
74 | 101 | if (val === 'assign:ME') { |
75 | 102 | const myId = session.value?.user?.id |
76 | 103 | return !!(myId && ride.volunteer?.userId === myId) |
77 | 104 | } |
78 | | - |
| 105 | +
|
79 | 106 | return false |
80 | 107 | }) |
81 | 108 | }) |
82 | 109 | } |
83 | 110 |
|
84 | 111 | // Date Range Filter |
85 | 112 | if (startDate.value) { |
86 | | - result = result.filter((ride: any) => new Date(ride.scheduledTime) >= new Date(startDate.value)) |
| 113 | + result = result.filter( |
| 114 | + (ride: any) => new Date(ride.scheduledTime) >= new Date(startDate.value) |
| 115 | + ) |
87 | 116 | } |
88 | 117 | if (endDate.value) { |
89 | 118 | const end = new Date(endDate.value) |
|
129 | 158 | id: 'volunteer', |
130 | 159 | header: 'Volunteer', |
131 | 160 | cell: ({ row }) => { |
132 | | - return row.original.volunteer?.user?.name || h('span', { class: 'text-gray-400 italic' }, 'Unassigned') |
| 161 | + return ( |
| 162 | + row.original.volunteer?.user?.name || |
| 163 | + h('span', { class: 'text-gray-400 italic' }, 'Unassigned') |
| 164 | + ) |
133 | 165 | }, |
134 | 166 | }, |
135 | 167 | { |
|
210 | 242 | multiple |
211 | 243 | :searchable="false" |
212 | 244 | :ui="{ input: 'hidden' }" |
213 | | - placeholder="Include Status / Volunteer" |
| 245 | + placeholder="Include Status" |
214 | 246 | class="w-full sm:w-64" |
215 | 247 | /> |
216 | 248 | <USelectMenu |
|
219 | 251 | multiple |
220 | 252 | :searchable="false" |
221 | 253 | :ui="{ input: 'hidden' }" |
222 | | - placeholder="Exclude Status / Volunteer" |
| 254 | + placeholder="Exclude Status" |
223 | 255 | class="w-full sm:w-64" |
224 | 256 | /> |
225 | 257 | <div class="flex items-center gap-2"> |
226 | | - <UInput |
227 | | - v-model="startDate" |
228 | | - type="date" |
229 | | - placeholder="Start" |
230 | | - class="w-full sm:w-auto" |
231 | | - /> |
| 258 | + <UInput v-model="startDate" type="date" placeholder="Start" class="w-full sm:w-auto" /> |
232 | 259 | <span class="text-gray-400">-</span> |
233 | | - <UInput |
234 | | - v-model="endDate" |
235 | | - type="date" |
236 | | - placeholder="End" |
237 | | - class="w-full sm:w-auto" |
238 | | - /> |
| 260 | + <UInput v-model="endDate" type="date" placeholder="End" class="w-full sm:w-auto" /> |
239 | 261 | </div> |
240 | 262 | </div> |
241 | 263 |
|
|
247 | 269 | @select="onSelect" |
248 | 270 | /> |
249 | 271 |
|
250 | | - <!-- Create Ride Modal --> |
251 | | - <UModal v-model:open="isCreateModalOpen" title="Create New Ride"> |
252 | | - <template #content> |
253 | | - <div class="max-h-[70vh] overflow-y-auto p-4"> |
254 | | - <UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit"> |
255 | | - <UFormField label="Client" name="clientId"> |
256 | | - <USelect |
257 | | - v-model="state.clientId" |
258 | | - :items="clients?.map((c) => ({ label: c.user.name, value: c.id })) || []" |
259 | | - placeholder="Select a client" |
260 | | - class="w-full" |
261 | | - /> |
262 | | - </UFormField> |
263 | | - |
264 | | - <div class="rounded-lg border p-4 space-y-2 dark:border-gray-700"> |
265 | | - <h3 class="font-bold text-sm text-gray-700 dark:text-gray-300">Pickup Address</h3> |
266 | | - |
267 | | - <!-- Custom Autocomplete --> |
268 | | - <div class="relative mb-2"> |
269 | | - <UInput |
270 | | - v-model="pickupSearch" |
271 | | - placeholder="Type to find existing address (e.g. Street)..." |
272 | | - icon="i-lucide-search" |
273 | | - autocomplete="off" |
274 | | - /> |
275 | | - <div |
276 | | - v-if="pickupOptions?.length > 0 && pickupSearch" |
277 | | - class="absolute z-10 mt-1 w-full max-h-60 overflow-auto rounded-md border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800" |
278 | | - > |
279 | | - <button |
280 | | - v-for="opt in pickupOptions" |
281 | | - :key="opt.id" |
282 | | - type="button" |
283 | | - class="block w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700" |
284 | | - @click="onPickupSelect(opt)" |
285 | | - > |
286 | | - {{ opt.label }} |
287 | | - </button> |
288 | | - </div> |
289 | | - </div> |
290 | | - |
291 | | - <UFormField label="Street" name="pickup.street"> |
292 | | - <UInput v-model="state.pickup.street" placeholder="Street Address" /> |
293 | | - </UFormField> <div class="grid grid-cols-3 gap-2"> |
294 | | - <UFormField label="City" name="pickup.city"><UInput v-model="state.pickup.city" placeholder="City" /></UFormField> |
295 | | - <UFormField label="State" name="pickup.state"><UInput v-model="state.pickup.state" placeholder="State" /></UFormField> |
296 | | - <UFormField label="Zip" name="pickup.zip"><UInput v-model="state.pickup.zip" placeholder="Zip" /></UFormField> |
297 | | - </div> |
298 | | - </div> |
299 | | - |
300 | | - <div class="rounded-lg border p-4 space-y-2 dark:border-gray-700"> |
301 | | - <h3 class="font-bold text-sm text-gray-700 dark:text-gray-300">Dropoff Address</h3> |
302 | | - |
303 | | - <!-- Custom Autocomplete --> |
304 | | - <div class="relative mb-2"> |
305 | | - <UInput |
306 | | - v-model="dropoffSearch" |
307 | | - placeholder="Type to find existing address (e.g. Street)..." |
308 | | - icon="i-lucide-search" |
309 | | - autocomplete="off" |
| 272 | + <!-- Create Ride Modal --> |
| 273 | + <UModal v-model:open="isCreateModalOpen" title="Create New Ride"> |
| 274 | + <template #content> |
| 275 | + <div class="max-h-[70vh] overflow-y-auto p-4"> |
| 276 | + <UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit"> |
| 277 | + <UFormField label="Client" name="clientId"> |
| 278 | + <USelect |
| 279 | + v-model="state.clientId" |
| 280 | + :items="clients?.map((c) => ({ label: c.user.name, value: c.id })) || []" |
| 281 | + placeholder="Select a client" |
| 282 | + class="w-full" |
310 | 283 | /> |
311 | | - <div |
312 | | - v-if="dropoffOptions?.length > 0 && dropoffSearch" |
313 | | - class="absolute z-10 mt-1 w-full max-h-60 overflow-auto rounded-md border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800" |
314 | | - > |
315 | | - <button |
316 | | - v-for="opt in dropoffOptions" |
317 | | - :key="opt.id" |
318 | | - type="button" |
319 | | - class="block w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700" |
320 | | - @click="onDropoffSelect(opt)" |
| 284 | + </UFormField> |
| 285 | + |
| 286 | + <div class="space-y-2 rounded-lg border p-4 dark:border-gray-700"> |
| 287 | + <h3 class="text-sm font-bold text-gray-700 dark:text-gray-300">Pickup Address</h3> |
| 288 | + |
| 289 | + <!-- Custom Autocomplete --> |
| 290 | + <div class="relative mb-2"> |
| 291 | + <UInput |
| 292 | + v-model="pickupSearch" |
| 293 | + placeholder="Type to find existing address (e.g. Street)..." |
| 294 | + icon="i-lucide-search" |
| 295 | + autocomplete="off" |
| 296 | + /> |
| 297 | + <div |
| 298 | + v-if="pickupOptions?.length > 0 && pickupSearch" |
| 299 | + class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800" |
321 | 300 | > |
322 | | - {{ opt.label }} |
323 | | - </button> |
| 301 | + <button |
| 302 | + v-for="opt in pickupOptions" |
| 303 | + :key="opt.id" |
| 304 | + type="button" |
| 305 | + class="block w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700" |
| 306 | + @click="onPickupSelect(opt)" |
| 307 | + > |
| 308 | + {{ opt.label }} |
| 309 | + </button> |
| 310 | + </div> |
| 311 | + </div> |
| 312 | + |
| 313 | + <UFormField label="Street" name="pickup.street"> |
| 314 | + <UInput v-model="state.pickup.street" placeholder="Street Address" /> |
| 315 | + </UFormField> |
| 316 | + <div class="grid grid-cols-3 gap-2"> |
| 317 | + <UFormField label="City" name="pickup.city" |
| 318 | + ><UInput v-model="state.pickup.city" placeholder="City" |
| 319 | + /></UFormField> |
| 320 | + <UFormField label="State" name="pickup.state" |
| 321 | + ><UInput v-model="state.pickup.state" placeholder="State" |
| 322 | + /></UFormField> |
| 323 | + <UFormField label="Zip" name="pickup.zip" |
| 324 | + ><UInput v-model="state.pickup.zip" placeholder="Zip" |
| 325 | + /></UFormField> |
324 | 326 | </div> |
325 | 327 | </div> |
326 | 328 |
|
327 | | - <UFormField label="Street" name="dropoff.street"> |
328 | | - <UInput v-model="state.dropoff.street" placeholder="Street Address" /> |
329 | | - </UFormField> |
330 | | - <div class="grid grid-cols-3 gap-2"> |
331 | | - <UFormField label="City" name="dropoff.city"><UInput v-model="state.dropoff.city" placeholder="City" /></UFormField> |
332 | | - <UFormField label="State" name="dropoff.state"><UInput v-model="state.dropoff.state" placeholder="State" /></UFormField> |
333 | | - <UFormField label="Zip" name="dropoff.zip"><UInput v-model="state.dropoff.zip" placeholder="Zip" /></UFormField> |
| 329 | + <div class="space-y-2 rounded-lg border p-4 dark:border-gray-700"> |
| 330 | + <h3 class="text-sm font-bold text-gray-700 dark:text-gray-300">Dropoff Address</h3> |
| 331 | + |
| 332 | + <!-- Custom Autocomplete --> |
| 333 | + <div class="relative mb-2"> |
| 334 | + <UInput |
| 335 | + v-model="dropoffSearch" |
| 336 | + placeholder="Type to find existing address (e.g. Street)..." |
| 337 | + icon="i-lucide-search" |
| 338 | + autocomplete="off" |
| 339 | + /> |
| 340 | + <div |
| 341 | + v-if="dropoffOptions?.length > 0 && dropoffSearch" |
| 342 | + class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800" |
| 343 | + > |
| 344 | + <button |
| 345 | + v-for="opt in dropoffOptions" |
| 346 | + :key="opt.id" |
| 347 | + type="button" |
| 348 | + class="block w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700" |
| 349 | + @click="onDropoffSelect(opt)" |
| 350 | + > |
| 351 | + {{ opt.label }} |
| 352 | + </button> |
| 353 | + </div> |
| 354 | + </div> |
| 355 | + |
| 356 | + <UFormField label="Street" name="dropoff.street"> |
| 357 | + <UInput v-model="state.dropoff.street" placeholder="Street Address" /> |
| 358 | + </UFormField> |
| 359 | + <div class="grid grid-cols-3 gap-2"> |
| 360 | + <UFormField label="City" name="dropoff.city" |
| 361 | + ><UInput v-model="state.dropoff.city" placeholder="City" |
| 362 | + /></UFormField> |
| 363 | + <UFormField label="State" name="dropoff.state" |
| 364 | + ><UInput v-model="state.dropoff.state" placeholder="State" |
| 365 | + /></UFormField> |
| 366 | + <UFormField label="Zip" name="dropoff.zip" |
| 367 | + ><UInput v-model="state.dropoff.zip" placeholder="Zip" |
| 368 | + /></UFormField> |
| 369 | + </div> |
334 | 370 | </div> |
335 | | - </div> |
336 | | - |
337 | | - <UFormField label="Scheduled Time" name="scheduledTime"> |
338 | | - <UInput v-model="state.scheduledTime" type="datetime-local" class="w-full" /> |
339 | | - </UFormField> |
340 | | - |
341 | | - <UFormField label="Notes" name="notes"> |
342 | | - <UTextarea |
343 | | - v-model="state.notes" |
344 | | - placeholder="Additional instructions..." |
345 | | - class="w-full" |
346 | | - /> |
347 | | - </UFormField> |
| 371 | + |
| 372 | + <UFormField label="Scheduled Time" name="scheduledTime"> |
| 373 | + <UInput v-model="state.scheduledTime" type="datetime-local" class="w-full" /> |
| 374 | + </UFormField> |
| 375 | + |
| 376 | + <UFormField label="Notes" name="notes"> |
| 377 | + <UTextarea |
| 378 | + v-model="state.notes" |
| 379 | + placeholder="Additional instructions..." |
| 380 | + class="w-full" |
| 381 | + /> |
| 382 | + </UFormField> |
348 | 383 |
|
349 | 384 | <div class="flex justify-end gap-2 pt-4"> |
350 | 385 | <UButton |
|
0 commit comments