fixed itemized selection and added toasts

This commit is contained in:
chark1es 2025-02-24 12:39:27 -08:00
parent d74f403e34
commit 2501fe4ed8
8 changed files with 439 additions and 173 deletions

View file

@ -3,6 +3,9 @@ import { Authentication } from "../../scripts/pocketbase/Authentication";
import { Update } from "../../scripts/pocketbase/Update";
import { FileManager } from "../../scripts/pocketbase/FileManager";
import { Get } from "../../scripts/pocketbase/Get";
import { toast } from "react-hot-toast";
import Toast from "./universal/Toast";
import { Icon } from "@iconify/react";
// Form sections
import PRSection from "./Officer_EventRequestForm/PRSection";
@ -55,6 +58,7 @@ if (auth.isAuthenticated()) {
---
<div class="w-full max-w-4xl mx-auto p-6">
<Toast client:load />
<h1
class="text-3xl font-bold mb-8 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
>
@ -286,6 +290,8 @@ if (auth.isAuthenticated()) {
import { Update } from "../../scripts/pocketbase/Update";
import { FileManager } from "../../scripts/pocketbase/FileManager";
import { Get } from "../../scripts/pocketbase/Get";
import { toast } from "react-hot-toast";
import { showLoadingToast, showSuccessToast, showErrorToast } from "./Officer_EventRequestForm/ToastNotifications";
// Add TypeScript interfaces
interface EventRequest {
@ -330,86 +336,104 @@ if (auth.isAuthenticated()) {
form?.addEventListener("submit", async (e) => {
e.preventDefault();
// Collect form data
const formData = new FormData(form);
const data: Record<string, any> = {};
// Convert FormData to a proper object with correct types
formData.forEach((value, key) => {
if (value instanceof File) {
// Skip file fields as they'll be handled separately
return;
}
data[key] = value;
});
try {
// Create event request record
const auth = Authentication.getInstance();
const update = Update.getInstance();
// Add user ID to the request
const userId = auth.getUserId();
if (userId) {
data.requested_user = userId;
if (!userId) {
throw new Error('User not authenticated');
}
// Show loading toast
const loadingToastId = toast.custom(showLoadingToast, { duration: 0 });
// Get graphics need value
const needsGraphics = form.querySelector<HTMLInputElement>('input[name="needsGraphics"]:checked')?.value === "yes";
// Collect data from all sections
const eventData = {
requested_user: userId,
name: form.querySelector<HTMLInputElement>('[name="event_name"]')?.value,
description: form.querySelector<HTMLTextAreaElement>('[name="event_description"]')?.value,
location: form.querySelector<HTMLInputElement>('[name="location"]')?.value,
start_date_time: new Date(form.querySelector<HTMLInputElement>('[name="start_date_time"]')?.value || '').toISOString(),
end_date_time: new Date(form.querySelector<HTMLInputElement>('[name="end_date_time"]')?.value || '').toISOString(),
flyers_needed: needsGraphics,
flyer_type: Array.from(form.querySelectorAll<HTMLInputElement>('input[type="checkbox"][name="flyer_type[]"]:checked')).map(cb => cb.value),
other_flyer_type: form.querySelector<HTMLInputElement>('[name="other_flyer_type"]')?.value,
flyer_advertising_start_date: form.querySelector<HTMLInputElement>('[name="flyer_advertising_start_date"]')?.value,
flyer_additional_requests: form.querySelector<HTMLTextAreaElement>('[name="flyer_additional_requests"]')?.value,
photography_needed: form.querySelector<HTMLInputElement>('input[name="photography_needed"]:checked')?.value === 'true',
required_logos: Array.from(form.querySelectorAll<HTMLInputElement>('input[type="checkbox"][name="required_logos[]"]:checked')).map(cb => cb.value),
advertising_format: form.querySelector<HTMLSelectElement>('[name="advertising_format"]')?.value,
will_or_have_room_booking: form.querySelector<HTMLInputElement>('input[name="will_or_have_room_booking"]:checked')?.value === 'true',
expected_attendance: parseInt(form.querySelector<HTMLInputElement>('[name="expected_attendance"]')?.value || '0'),
as_funding_required: form.querySelector<HTMLInputElement>('input[name="as_funding"]:checked')?.value === 'true',
food_drinks_being_served: form.querySelector<HTMLInputElement>('input[name="food_drinks"]:checked')?.value === 'true',
status: 'pending',
draft: false,
// Add itemized items if AS funding is required
itemized_items: form.querySelector<HTMLInputElement>('input[name="as_funding"]:checked')?.value === 'true'
? JSON.parse(form.querySelector<HTMLInputElement>('[name="itemized_items"]')?.value || '[]')
: undefined,
// Add itemized invoice data
itemized_invoice: form.querySelector<HTMLInputElement>('input[name="as_funding"]:checked')?.value === 'true'
? JSON.parse(form.querySelector<HTMLInputElement>('[name="itemized_invoice"]')?.value || '{}')
: undefined
};
// Create the record
const record = await update.updateFields(
"event_request",
data.id || "",
data,
);
const record = await update.create("event_request", eventData);
// Handle file uploads if any
// Handle file uploads
const fileManager = FileManager.getInstance();
const fileFields = ["room_booking", "invoice", "other_logos"];
const fileFields = {
room_booking: form.querySelector<HTMLInputElement>('input[type="file"][name="room_booking"]')?.files?.[0],
itemized_invoice: form.querySelector<HTMLInputElement>('input[type="file"][name="itemized_invoice"]')?.files?.[0],
invoice: form.querySelector<HTMLInputElement>('input[type="file"][name="invoice"]')?.files?.[0],
other_logos: Array.from(form.querySelector<HTMLInputElement>('input[type="file"][name="other_logos"]')?.files || [])
};
for (const field of fileFields) {
const files = formData
.getAll(field)
.filter((f): f is File => f instanceof File);
if (files.length > 0) {
await fileManager.uploadFiles(
// Upload files one by one
for (const [field, files] of Object.entries(fileFields)) {
if (!files) continue;
if (Array.isArray(files)) {
// Handle multiple files (other_logos)
const validFiles = files.filter((f): f is File => f instanceof File);
if (validFiles.length > 0) {
await fileManager.uploadFiles(
"event_request",
record.id,
field,
validFiles
);
}
} else {
// Handle single file
await fileManager.uploadFile(
"event_request",
record.id,
field,
files,
files
);
}
}
// Show success message using a toast
const toast = document.createElement("div");
toast.className = "toast toast-end";
toast.innerHTML = `
<div class="alert alert-success">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Event request submitted successfully!</span>
</div>
`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
// Dismiss loading toast
toast.dismiss(loadingToastId);
// Redirect to events management page
window.location.href = "/dashboard/events";
// Show success toast
toast.custom(() => showSuccessToast('Event request submitted successfully!'));
// Reset form
form.reset();
} catch (error) {
console.error("Error submitting form:", error);
// Show error toast
const toast = document.createElement("div");
toast.className = "toast toast-end";
toast.innerHTML = `
<div class="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
<span>Error submitting form. Please try again.</span>
</div>
`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
toast.custom(() => showErrorToast('Failed to submit event request. Please try again.'));
}
});
@ -417,22 +441,21 @@ if (auth.isAuthenticated()) {
document
.getElementById("saveAsDraft")
?.addEventListener("click", async () => {
// Similar to submit but mark as draft
const formData = new FormData(form);
const data: Record<string, any> = {};
// Convert FormData to a proper object with correct types
formData.forEach((value, key) => {
if (value instanceof File) {
// Skip file fields as they'll be handled separately
return;
}
data[key] = value;
});
data.status = "draft";
try {
const formData = new FormData(form);
const data: Record<string, any> = {};
// Convert FormData to a proper object with correct types
formData.forEach((value, key) => {
if (value instanceof File) {
// Skip file fields as they'll be handled separately
return;
}
data[key] = value;
});
data.status = "draft";
const auth = Authentication.getInstance();
const update = Update.getInstance();
@ -444,33 +467,11 @@ if (auth.isAuthenticated()) {
await update.updateFields("event_request", data.id || "", data);
// Show success toast
const toast = document.createElement("div");
toast.className = "toast toast-end";
toast.innerHTML = `
<div class="alert alert-success">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Draft saved successfully!</span>
</div>
`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
toast.custom(() => showSuccessToast('Draft saved successfully!'));
} catch (error) {
console.error("Error saving draft:", error);
// Show error toast
const toast = document.createElement("div");
toast.className = "toast toast-end";
toast.innerHTML = `
<div class="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
<span>Error saving draft. Please try again.</span>
</div>
`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
toast.custom(() => showErrorToast('Error saving draft. Please try again.'));
}
});

View file

@ -6,7 +6,7 @@ import Tooltip from './Tooltip';
import { tooltips, infoNotes } from './tooltips';
import { Icon } from '@iconify/react';
interface InvoiceItem {
export interface InvoiceItem {
quantity: number;
item_name: string;
unit_cost: number;
@ -20,13 +20,14 @@ interface InvoiceData {
vendor: string;
}
interface ASFundingSectionProps {
export interface ASFundingSectionProps {
onDataChange?: (data: any) => void;
onItemizedItemsUpdate?: (items: InvoiceItem[]) => void;
}
const ASFundingSection: React.FC<ASFundingSectionProps> = ({ onDataChange }) => {
const ASFundingSection: React.FC<ASFundingSectionProps> = ({ onDataChange, onItemizedItemsUpdate }) => {
const [invoiceData, setInvoiceData] = useState<InvoiceData>({
items: [{ quantity: 0, item_name: '', unit_cost: 0 }],
items: [{ quantity: 1, item_name: '', unit_cost: 0 }],
tax: 0,
tip: 0,
total: 0,
@ -39,83 +40,99 @@ const ASFundingSection: React.FC<ASFundingSectionProps> = ({ onDataChange }) =>
// Calculate new total
const itemsTotal = newItems.reduce((sum, item) => sum + (item.quantity * item.unit_cost), 0);
const newTotal = itemsTotal + invoiceData.tax + invoiceData.tip;
const newTotal = itemsTotal + (invoiceData.tax || 0) + (invoiceData.tip || 0);
setInvoiceData(prev => ({
...prev,
const newInvoiceData = {
...invoiceData,
items: newItems,
total: newTotal
}));
};
// Notify parent with JSON string
setInvoiceData(newInvoiceData);
// Send the entire invoice data object
onDataChange?.({
itemized_invoice: JSON.stringify({
...invoiceData,
items: newItems,
total: newTotal
})
itemized_invoice: newInvoiceData,
total_amount: newTotal
});
// Update parent with itemized items and invoice data
onItemizedItemsUpdate?.(newItems);
document.querySelector<HTMLInputElement>('input[name="itemized_invoice"]')?.setAttribute('value', JSON.stringify(newInvoiceData));
};
const addItem = () => {
setInvoiceData(prev => ({
...prev,
items: [...prev.items, { quantity: 0, item_name: '', unit_cost: 0 }]
}));
toast('New item added', { icon: '' });
const newItems = [...invoiceData.items, { quantity: 1, item_name: '', unit_cost: 0 }];
const itemsTotal = newItems.reduce((sum, item) => sum + (item.quantity * item.unit_cost), 0);
const newTotal = itemsTotal + (invoiceData.tax || 0) + (invoiceData.tip || 0);
const newInvoiceData = {
...invoiceData,
items: newItems,
total: newTotal
};
setInvoiceData(newInvoiceData);
onDataChange?.({
itemized_invoice: newInvoiceData,
total_amount: newTotal
});
onItemizedItemsUpdate?.(newItems);
document.querySelector<HTMLInputElement>('input[name="itemized_invoice"]')?.setAttribute('value', JSON.stringify(newInvoiceData));
toast('New item added');
};
const removeItem = (index: number) => {
if (invoiceData.items.length > 1) {
const newItems = invoiceData.items.filter((_, i) => i !== index);
// Recalculate total
const itemsTotal = newItems.reduce((sum, item) => sum + (item.quantity * item.unit_cost), 0);
const newTotal = itemsTotal + invoiceData.tax + invoiceData.tip;
const newTotal = itemsTotal + (invoiceData.tax || 0) + (invoiceData.tip || 0);
setInvoiceData(prev => ({
...prev,
const newInvoiceData = {
...invoiceData,
items: newItems,
total: newTotal
}));
};
// Notify parent with JSON string
setInvoiceData(newInvoiceData);
onDataChange?.({
itemized_invoice: JSON.stringify({
...invoiceData,
items: newItems,
total: newTotal
})
itemized_invoice: newInvoiceData,
total_amount: newTotal
});
toast('Item removed', { icon: '🗑️' });
onItemizedItemsUpdate?.(newItems);
document.querySelector<HTMLInputElement>('input[name="itemized_invoice"]')?.setAttribute('value', JSON.stringify(newInvoiceData));
toast('Item removed');
}
};
const handleExtraChange = (field: 'tax' | 'tip' | 'vendor', value: string | number) => {
const numValue = field !== 'vendor' ? Number(value) : value;
// Calculate new total for tax/tip changes
const itemsTotal = invoiceData.items.reduce((sum, item) => sum + (item.quantity * item.unit_cost), 0);
const newTotal = field === 'tax' ?
itemsTotal + Number(value) + invoiceData.tip :
itemsTotal + Number(value) + (invoiceData.tip || 0) :
field === 'tip' ?
itemsTotal + invoiceData.tax + Number(value) :
itemsTotal + invoiceData.tax + invoiceData.tip;
itemsTotal + (invoiceData.tax || 0) + Number(value) :
itemsTotal + (invoiceData.tax || 0) + (invoiceData.tip || 0);
setInvoiceData(prev => ({
...prev,
const newInvoiceData = {
...invoiceData,
[field]: numValue,
total: field !== 'vendor' ? newTotal : prev.total
}));
total: field !== 'vendor' ? newTotal : invoiceData.total
};
// Notify parent with JSON string
setInvoiceData(newInvoiceData);
onDataChange?.({
itemized_invoice: JSON.stringify({
...invoiceData,
[field]: numValue,
total: field !== 'vendor' ? newTotal : invoiceData.total
})
itemized_invoice: newInvoiceData,
total_amount: newTotal
});
document.querySelector<HTMLInputElement>('input[name="itemized_invoice"]')?.setAttribute('value', JSON.stringify(newInvoiceData));
};
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
onDataChange?.({ invoice: file });
toast('Invoice file uploaded');
}
};
return (
@ -438,14 +455,9 @@ const ASFundingSection: React.FC<ASFundingSectionProps> = ({ onDataChange }) =>
<input
type="file"
name="invoice"
className="file-input file-input-bordered file-input-primary w-full"
onChange={handleFileUpload}
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"
className="file-input file-input-bordered w-full pl-12 transition-all duration-300 focus:ring-2 focus:ring-primary/20"
onChange={(e) => {
onDataChange?.({ invoice: e.target.files?.[0] });
if (e.target.files?.[0]) {
toast('Invoice file uploaded', { icon: '📄' });
}
}}
required
/>
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">

View file

@ -27,7 +27,7 @@ const EventDetailsSection: React.FC<EventDetailsSectionProps> = ({ onDataChange
</label>
<input
type="text"
name="name"
name="event_name"
className="input input-bordered w-full"
onChange={(e) => onDataChange?.({ name: e.target.value })}
required
@ -44,7 +44,7 @@ const EventDetailsSection: React.FC<EventDetailsSectionProps> = ({ onDataChange
</span>
</label>
<textarea
name="description"
name="event_description"
className="textarea textarea-bordered h-32"
onChange={(e) => onDataChange?.({ description: e.target.value })}
required

View file

@ -1,4 +1,6 @@
import React, { useState } from 'react';
import { toast } from 'react-hot-toast';
import { Icon } from '@iconify/react';
interface PRSectionProps {
onDataChange?: (data: any) => void;
@ -50,6 +52,14 @@ const PRSection: React.FC<PRSectionProps> = ({ onDataChange }) => {
}
};
const handleOtherLogosUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files && files.length > 0) {
onDataChange?.({ other_logos: files });
toast.success(`${files.length} logo file(s) uploaded`);
}
};
return (
<div className="card bg-base-100/95 backdrop-blur-md shadow-lg">
<div className="card-body">
@ -76,12 +86,26 @@ const PRSection: React.FC<PRSectionProps> = ({ onDataChange }) => {
<input
type="checkbox"
className="checkbox checkbox-primary"
name="flyer_type[]"
value={type.value}
checked={selectedTypes.includes(type.value)}
onChange={() => handleTypeChange(type.value)}
/>
<span className="label-text">{type.label}</span>
</label>
))}
{selectedTypes.includes('other') && (
<div className="form-control w-full mt-2 ml-10">
<input
type="text"
name="other_flyer_type"
placeholder="Please specify other flyer type"
className="input input-bordered w-full"
onChange={(e) => onDataChange?.({ other_flyer_type: e.target.value })}
/>
</div>
)}
</div>
</label>
</div>
@ -120,6 +144,8 @@ const PRSection: React.FC<PRSectionProps> = ({ onDataChange }) => {
<input
type="checkbox"
className="checkbox checkbox-primary"
name="required_logos[]"
value={logo.value}
checked={selectedLogos.includes(logo.value)}
onChange={() => handleLogoChange(logo.value)}
/>
@ -133,9 +159,7 @@ const PRSection: React.FC<PRSectionProps> = ({ onDataChange }) => {
<div className="form-control w-full">
<label className="label">
<span className="label-text font-medium text-lg flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<Icon icon="mdi:upload" className="h-5 w-5 text-primary" />
Upload Logo Files
</span>
</label>
@ -145,7 +169,7 @@ const PRSection: React.FC<PRSectionProps> = ({ onDataChange }) => {
multiple
accept="image/*"
className="file-input file-input-bordered w-full"
onChange={(e) => onDataChange?.({ other_logos: e.target.files })}
onChange={handleOtherLogosUpload}
/>
</div>
)}

View file

@ -5,11 +5,22 @@ import InfoCard from './InfoCard';
import Tooltip from './Tooltip';
import { tooltips, infoNotes } from './tooltips';
import { Icon } from '@iconify/react';
import { FileManager } from '../../../scripts/pocketbase/FileManager';
import Toast from '../universal/Toast';
import type { ASFundingSectionProps, InvoiceItem } from './ASFundingSection';
interface TAPSectionProps {
onDataChange?: (data: any) => void;
onASFundingChange?: (enabled: boolean) => void;
children?: React.ReactNode;
children?: React.ReactElement<ASFundingSectionProps>;
}
interface TAPData {
expected_attendance: number;
room_booking: string | File;
as_funding_required: boolean;
food_drinks_being_served: boolean;
itemized_items?: InvoiceItem[];
}
const TAPSection: React.FC<TAPSectionProps> = ({ onDataChange, onASFundingChange, children }) => {
@ -17,14 +28,19 @@ const TAPSection: React.FC<TAPSectionProps> = ({ onDataChange, onASFundingChange
const [roomBooking, setRoomBooking] = useState<string>('');
const [needsASFunding, setNeedsASFunding] = useState<boolean>(false);
const [needsFoodDrinks, setNeedsFoodDrinks] = useState<boolean>(false);
const [roomBookingFile, setRoomBookingFile] = useState<File | null>(null);
const [itemizedItems, setItemizedItems] = useState<InvoiceItem[]>([]);
const fileManager = FileManager.getInstance();
const handleAttendanceChange = (value: number) => {
setExpectedAttendance(value);
if (value > 100) {
toast('Large attendance detected! Please ensure proper room capacity.', {
icon: '⚠️',
duration: 4000
});
toast.custom((t) => (
<div className="alert alert-warning">
<Icon icon="mdi:warning" className="h-6 w-6" />
<span>Large attendance detected! Please ensure proper room capacity.</span>
</div>
), { duration: 4000 });
}
onDataChange?.({ expected_attendance: value });
};
@ -38,20 +54,66 @@ const TAPSection: React.FC<TAPSectionProps> = ({ onDataChange, onASFundingChange
setNeedsASFunding(enabled);
if (!enabled) {
setNeedsFoodDrinks(false);
onDataChange?.({ needs_food_drinks: false });
setItemizedItems([]);
onDataChange?.({ food_drinks_being_served: false });
}
onASFundingChange?.(enabled);
onDataChange?.({ as_funding_required: enabled });
toast(enabled ? 'AS Funding enabled - please fill out funding details.' : 'AS Funding disabled', {
icon: enabled ? '💰' : '❌',
duration: 3000
onDataChange?.({
as_funding_required: enabled,
itemized_items: enabled ? itemizedItems : undefined
});
toast.custom((t) => (
<div className={`alert ${enabled ? 'alert-info' : 'alert-warning'}`}>
<Icon icon={enabled ? 'mdi:cash' : 'mdi:cash-off'} className="h-6 w-6" />
<span>{enabled ? 'AS Funding enabled - please fill out funding details.' : 'AS Funding disabled'}</span>
</div>
), { duration: 3000 });
};
const handleFoodDrinksChange = (enabled: boolean) => {
setNeedsFoodDrinks(enabled);
onDataChange?.({ needs_food_drinks: enabled });
onDataChange?.({ food_drinks_being_served: enabled });
};
const handleRoomBookingFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setRoomBookingFile(file);
onDataChange?.({ room_booking: file });
toast.custom((t) => (
<div className="alert alert-success">
<Icon icon="mdi:check-circle" className="h-6 w-6" />
<span>Room booking file uploaded successfully</span>
</div>
));
}
};
const uploadRoomBookingFile = async (recordId: string) => {
if (roomBookingFile) {
try {
await fileManager.uploadFile(
'event_request',
recordId,
'room_booking',
roomBookingFile
);
} catch (error) {
console.error('Failed to upload room booking file:', error);
toast.custom((t) => (
<div className="alert alert-error">
<Icon icon="mdi:error" className="h-6 w-6" />
<span>Failed to upload room booking file</span>
</div>
), { duration: 4000 });
}
}
};
const handleItemizedItemsUpdate = (items: InvoiceItem[]) => {
setItemizedItems(items);
onDataChange?.({ itemized_items: items });
};
return (
@ -97,14 +159,21 @@ const TAPSection: React.FC<TAPSectionProps> = ({ onDataChange, onASFundingChange
</div>
</Tooltip>
</div>
<InfoCard
title={infoNotes.funding.title}
items={infoNotes.funding.items}
type="warning"
className="mb-4"
/>
<div className="relative group">
<input
type="number"
min="0"
name="expected_attendance"
className="input input-bordered w-full pl-12 transition-all duration-300 focus:ring-2 focus:ring-primary/20"
value={expectedAttendance || ''}
onChange={(e) => handleAttendanceChange(Number(e.target.value))}
value={expectedAttendance}
onChange={(e) => handleAttendanceChange(parseInt(e.target.value) || 0)}
required
/>
</div>
@ -151,6 +220,27 @@ const TAPSection: React.FC<TAPSectionProps> = ({ onDataChange, onASFundingChange
required
/>
</div>
<div className="form-control w-full mt-4">
<label className="label">
<span className="label-text font-medium text-lg flex items-center gap-2">
<Icon icon="mdi:upload" className="h-5 w-5 text-primary" />
Room Booking File Upload
</span>
</label>
<div className="flex flex-col space-y-2">
<input
type="file"
name="room_booking"
className="file-input file-input-bordered file-input-primary w-full"
onChange={handleRoomBookingFileChange}
accept=".pdf,.jpg,.jpeg,.png"
/>
<div className="text-xs text-gray-500">
Max file size: 50MB
</div>
</div>
</div>
</motion.div>
<motion.div
@ -185,6 +275,7 @@ const TAPSection: React.FC<TAPSectionProps> = ({ onDataChange, onASFundingChange
className="radio radio-primary"
checked={needsASFunding}
onChange={() => handleASFundingChange(true)}
value="true"
/>
<span className="label-text">Yes, I need AS Funding</span>
</label>
@ -195,6 +286,7 @@ const TAPSection: React.FC<TAPSectionProps> = ({ onDataChange, onASFundingChange
className="radio radio-primary"
checked={!needsASFunding}
onChange={() => handleASFundingChange(false)}
value="false"
/>
<span className="label-text">No, I don't need AS Funding</span>
</label>
@ -236,6 +328,7 @@ const TAPSection: React.FC<TAPSectionProps> = ({ onDataChange, onASFundingChange
className="radio radio-primary"
checked={needsFoodDrinks}
onChange={() => handleFoodDrinksChange(true)}
value="true"
/>
<span className="label-text">Yes, I need food/drinks</span>
</label>
@ -246,6 +339,7 @@ const TAPSection: React.FC<TAPSectionProps> = ({ onDataChange, onASFundingChange
className="radio radio-primary"
checked={!needsFoodDrinks}
onChange={() => handleFoodDrinksChange(false)}
value="false"
/>
<span className="label-text">No, I don't need food/drinks</span>
</label>
@ -257,6 +351,24 @@ const TAPSection: React.FC<TAPSectionProps> = ({ onDataChange, onASFundingChange
</div>
</motion.div>
<input
type="hidden"
name="itemized_items"
value={JSON.stringify(itemizedItems)}
/>
<input
type="hidden"
name="itemized_invoice"
value={JSON.stringify({
items: itemizedItems,
tax: 0,
tip: 0,
total: itemizedItems.reduce((sum, item) => sum + (item.quantity * item.unit_cost), 0),
vendor: ''
})}
/>
<AnimatePresence mode="popLayout">
{needsASFunding && (
<motion.div
@ -271,7 +383,9 @@ const TAPSection: React.FC<TAPSectionProps> = ({ onDataChange, onASFundingChange
}}
className="mt-8"
>
{children}
{children && React.cloneElement(children, {
onItemizedItemsUpdate: handleItemizedItemsUpdate
})}
</motion.div>
)}
</AnimatePresence>

View file

@ -0,0 +1,37 @@
import React from 'react';
import { Icon } from '@iconify/react';
export const showLoadingToast = () => (
<div className="alert alert-info">
<Icon icon="mdi:loading" className="h-6 w-6 animate-spin" />
<span>Submitting event request...</span>
</div>
);
export const showSuccessToast = (message: string) => (
<div className="alert alert-success">
<Icon icon="mdi:check-circle" className="h-6 w-6" />
<span>{message}</span>
</div>
);
export const showErrorToast = (message: string) => (
<div className="alert alert-error">
<Icon icon="mdi:error" className="h-6 w-6" />
<span>{message}</span>
</div>
);
export const showWarningToast = (message: string) => (
<div className="alert alert-warning">
<Icon icon="mdi:warning" className="h-6 w-6" />
<span>{message}</span>
</div>
);
export const showInfoToast = (message: string) => (
<div className="alert alert-info">
<Icon icon="mdi:information" className="h-6 w-6" />
<span>{message}</span>
</div>
);

View file

@ -0,0 +1,44 @@
import React from 'react';
import { Toaster } from 'react-hot-toast';
// This is just a wrapper component to make react-hot-toast work with Astro
const Toast: React.FC = () => {
return (
<Toaster
position="top-center"
toastOptions={{
duration: 3000,
className: 'backdrop-blur-sm',
style: {
background: 'hsl(var(--b2, 0 0% 90%))',
color: 'hsl(var(--bc, 0 0% 20%))',
padding: '16px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
borderRadius: '0.75rem',
},
success: {
style: {
background: 'hsl(var(--su, 120 100% 90%))',
border: '1px solid hsl(var(--su, 120 100% 25%))',
},
iconTheme: {
primary: 'hsl(var(--su, 120 100% 25%))',
secondary: 'hsl(var(--b1, 0 0% 100%))',
},
},
error: {
style: {
background: 'hsl(var(--er, 0 100% 90%))',
border: '1px solid hsl(var(--er, 0 100% 25%))',
},
iconTheme: {
primary: 'hsl(var(--er, 0 100% 25%))',
secondary: 'hsl(var(--b1, 0 0% 100%))',
},
},
}}
/>
);
};
export default Toast;

View file

@ -42,6 +42,34 @@ export class Update {
return Update.instance;
}
/**
* Create a new record
* @param collectionName The name of the collection
* @param data The data for the new record
* @returns The created record
*/
public async create<T = any>(
collectionName: string,
data: Record<string, any>,
): Promise<T> {
if (!this.auth.isAuthenticated()) {
throw new Error("User must be authenticated to create records");
}
try {
this.auth.setUpdating(true);
const pb = this.auth.getPocketBase();
const convertedData = convertLocalToUTC(data);
const result = await pb.collection(collectionName).create<T>(convertedData);
return result;
} catch (err) {
console.error(`Failed to create record in ${collectionName}:`, err);
throw err;
} finally {
this.auth.setUpdating(false);
}
}
/**
* Update a single field in a record
* @param collectionName The name of the collection
@ -97,6 +125,12 @@ export class Update {
this.auth.setUpdating(true);
const pb = this.auth.getPocketBase();
const convertedUpdates = convertLocalToUTC(updates);
// If recordId is empty, create a new record instead of updating
if (!recordId) {
return this.create<T>(collectionName, convertedUpdates);
}
const result = await pb
.collection(collectionName)
.update<T>(recordId, convertedUpdates);