fixed itemized selection and added toasts
This commit is contained in:
parent
d74f403e34
commit
2501fe4ed8
8 changed files with 439 additions and 173 deletions
|
@ -3,6 +3,9 @@ import { Authentication } from "../../scripts/pocketbase/Authentication";
|
||||||
import { Update } from "../../scripts/pocketbase/Update";
|
import { Update } from "../../scripts/pocketbase/Update";
|
||||||
import { FileManager } from "../../scripts/pocketbase/FileManager";
|
import { FileManager } from "../../scripts/pocketbase/FileManager";
|
||||||
import { Get } from "../../scripts/pocketbase/Get";
|
import { Get } from "../../scripts/pocketbase/Get";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
import Toast from "./universal/Toast";
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
|
||||||
// Form sections
|
// Form sections
|
||||||
import PRSection from "./Officer_EventRequestForm/PRSection";
|
import PRSection from "./Officer_EventRequestForm/PRSection";
|
||||||
|
@ -55,6 +58,7 @@ if (auth.isAuthenticated()) {
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="w-full max-w-4xl mx-auto p-6">
|
<div class="w-full max-w-4xl mx-auto p-6">
|
||||||
|
<Toast client:load />
|
||||||
<h1
|
<h1
|
||||||
class="text-3xl font-bold mb-8 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
|
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 { Update } from "../../scripts/pocketbase/Update";
|
||||||
import { FileManager } from "../../scripts/pocketbase/FileManager";
|
import { FileManager } from "../../scripts/pocketbase/FileManager";
|
||||||
import { Get } from "../../scripts/pocketbase/Get";
|
import { Get } from "../../scripts/pocketbase/Get";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
import { showLoadingToast, showSuccessToast, showErrorToast } from "./Officer_EventRequestForm/ToastNotifications";
|
||||||
|
|
||||||
// Add TypeScript interfaces
|
// Add TypeScript interfaces
|
||||||
interface EventRequest {
|
interface EventRequest {
|
||||||
|
@ -330,86 +336,104 @@ if (auth.isAuthenticated()) {
|
||||||
form?.addEventListener("submit", async (e) => {
|
form?.addEventListener("submit", async (e) => {
|
||||||
e.preventDefault();
|
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 {
|
try {
|
||||||
// Create event request record
|
|
||||||
const auth = Authentication.getInstance();
|
const auth = Authentication.getInstance();
|
||||||
const update = Update.getInstance();
|
const update = Update.getInstance();
|
||||||
|
|
||||||
// Add user ID to the request
|
|
||||||
const userId = auth.getUserId();
|
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
|
// Create the record
|
||||||
const record = await update.updateFields(
|
const record = await update.create("event_request", eventData);
|
||||||
"event_request",
|
|
||||||
data.id || "",
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle file uploads if any
|
// Handle file uploads
|
||||||
const fileManager = FileManager.getInstance();
|
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) {
|
// Upload files one by one
|
||||||
const files = formData
|
for (const [field, files] of Object.entries(fileFields)) {
|
||||||
.getAll(field)
|
if (!files) continue;
|
||||||
.filter((f): f is File => f instanceof File);
|
|
||||||
if (files.length > 0) {
|
if (Array.isArray(files)) {
|
||||||
await fileManager.uploadFiles(
|
// 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",
|
"event_request",
|
||||||
record.id,
|
record.id,
|
||||||
field,
|
field,
|
||||||
files,
|
files
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show success message using a toast
|
// Dismiss loading toast
|
||||||
const toast = document.createElement("div");
|
toast.dismiss(loadingToastId);
|
||||||
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);
|
|
||||||
|
|
||||||
// Redirect to events management page
|
// Show success toast
|
||||||
window.location.href = "/dashboard/events";
|
toast.custom(() => showSuccessToast('Event request submitted successfully!'));
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
form.reset();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error submitting form:", error);
|
console.error("Error submitting form:", error);
|
||||||
|
|
||||||
// Show error toast
|
// Show error toast
|
||||||
const toast = document.createElement("div");
|
toast.custom(() => showErrorToast('Failed to submit event request. Please try again.'));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -417,22 +441,21 @@ if (auth.isAuthenticated()) {
|
||||||
document
|
document
|
||||||
.getElementById("saveAsDraft")
|
.getElementById("saveAsDraft")
|
||||||
?.addEventListener("click", async () => {
|
?.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 {
|
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 auth = Authentication.getInstance();
|
||||||
const update = Update.getInstance();
|
const update = Update.getInstance();
|
||||||
|
|
||||||
|
@ -444,33 +467,11 @@ if (auth.isAuthenticated()) {
|
||||||
await update.updateFields("event_request", data.id || "", data);
|
await update.updateFields("event_request", data.id || "", data);
|
||||||
|
|
||||||
// Show success toast
|
// Show success toast
|
||||||
const toast = document.createElement("div");
|
toast.custom(() => showSuccessToast('Draft saved successfully!'));
|
||||||
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);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving draft:", error);
|
console.error("Error saving draft:", error);
|
||||||
// Show error toast
|
// Show error toast
|
||||||
const toast = document.createElement("div");
|
toast.custom(() => showErrorToast('Error saving draft. Please try again.'));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import Tooltip from './Tooltip';
|
||||||
import { tooltips, infoNotes } from './tooltips';
|
import { tooltips, infoNotes } from './tooltips';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
interface InvoiceItem {
|
export interface InvoiceItem {
|
||||||
quantity: number;
|
quantity: number;
|
||||||
item_name: string;
|
item_name: string;
|
||||||
unit_cost: number;
|
unit_cost: number;
|
||||||
|
@ -20,13 +20,14 @@ interface InvoiceData {
|
||||||
vendor: string;
|
vendor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ASFundingSectionProps {
|
export interface ASFundingSectionProps {
|
||||||
onDataChange?: (data: any) => void;
|
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>({
|
const [invoiceData, setInvoiceData] = useState<InvoiceData>({
|
||||||
items: [{ quantity: 0, item_name: '', unit_cost: 0 }],
|
items: [{ quantity: 1, item_name: '', unit_cost: 0 }],
|
||||||
tax: 0,
|
tax: 0,
|
||||||
tip: 0,
|
tip: 0,
|
||||||
total: 0,
|
total: 0,
|
||||||
|
@ -39,83 +40,99 @@ const ASFundingSection: React.FC<ASFundingSectionProps> = ({ onDataChange }) =>
|
||||||
|
|
||||||
// Calculate new total
|
// Calculate new total
|
||||||
const itemsTotal = newItems.reduce((sum, item) => sum + (item.quantity * item.unit_cost), 0);
|
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 => ({
|
const newInvoiceData = {
|
||||||
...prev,
|
...invoiceData,
|
||||||
items: newItems,
|
items: newItems,
|
||||||
total: newTotal
|
total: newTotal
|
||||||
}));
|
};
|
||||||
|
|
||||||
// Notify parent with JSON string
|
setInvoiceData(newInvoiceData);
|
||||||
|
// Send the entire invoice data object
|
||||||
onDataChange?.({
|
onDataChange?.({
|
||||||
itemized_invoice: JSON.stringify({
|
itemized_invoice: newInvoiceData,
|
||||||
...invoiceData,
|
total_amount: newTotal
|
||||||
items: newItems,
|
|
||||||
total: 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 = () => {
|
const addItem = () => {
|
||||||
setInvoiceData(prev => ({
|
const newItems = [...invoiceData.items, { quantity: 1, item_name: '', unit_cost: 0 }];
|
||||||
...prev,
|
const itemsTotal = newItems.reduce((sum, item) => sum + (item.quantity * item.unit_cost), 0);
|
||||||
items: [...prev.items, { quantity: 0, item_name: '', unit_cost: 0 }]
|
const newTotal = itemsTotal + (invoiceData.tax || 0) + (invoiceData.tip || 0);
|
||||||
}));
|
|
||||||
toast('New item added', { icon: '➕' });
|
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) => {
|
const removeItem = (index: number) => {
|
||||||
if (invoiceData.items.length > 1) {
|
if (invoiceData.items.length > 1) {
|
||||||
const newItems = invoiceData.items.filter((_, i) => i !== index);
|
const newItems = invoiceData.items.filter((_, i) => i !== index);
|
||||||
|
|
||||||
// Recalculate total
|
|
||||||
const itemsTotal = newItems.reduce((sum, item) => sum + (item.quantity * item.unit_cost), 0);
|
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 => ({
|
const newInvoiceData = {
|
||||||
...prev,
|
...invoiceData,
|
||||||
items: newItems,
|
items: newItems,
|
||||||
total: newTotal
|
total: newTotal
|
||||||
}));
|
};
|
||||||
|
|
||||||
// Notify parent with JSON string
|
setInvoiceData(newInvoiceData);
|
||||||
onDataChange?.({
|
onDataChange?.({
|
||||||
itemized_invoice: JSON.stringify({
|
itemized_invoice: newInvoiceData,
|
||||||
...invoiceData,
|
total_amount: newTotal
|
||||||
items: newItems,
|
|
||||||
total: 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 handleExtraChange = (field: 'tax' | 'tip' | 'vendor', value: string | number) => {
|
||||||
const numValue = field !== 'vendor' ? Number(value) : value;
|
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 itemsTotal = invoiceData.items.reduce((sum, item) => sum + (item.quantity * item.unit_cost), 0);
|
||||||
|
|
||||||
const newTotal = field === 'tax' ?
|
const newTotal = field === 'tax' ?
|
||||||
itemsTotal + Number(value) + invoiceData.tip :
|
itemsTotal + Number(value) + (invoiceData.tip || 0) :
|
||||||
field === 'tip' ?
|
field === 'tip' ?
|
||||||
itemsTotal + invoiceData.tax + Number(value) :
|
itemsTotal + (invoiceData.tax || 0) + Number(value) :
|
||||||
itemsTotal + invoiceData.tax + invoiceData.tip;
|
itemsTotal + (invoiceData.tax || 0) + (invoiceData.tip || 0);
|
||||||
|
|
||||||
setInvoiceData(prev => ({
|
const newInvoiceData = {
|
||||||
...prev,
|
...invoiceData,
|
||||||
[field]: numValue,
|
[field]: numValue,
|
||||||
total: field !== 'vendor' ? newTotal : prev.total
|
total: field !== 'vendor' ? newTotal : invoiceData.total
|
||||||
}));
|
};
|
||||||
|
|
||||||
// Notify parent with JSON string
|
setInvoiceData(newInvoiceData);
|
||||||
onDataChange?.({
|
onDataChange?.({
|
||||||
itemized_invoice: JSON.stringify({
|
itemized_invoice: newInvoiceData,
|
||||||
...invoiceData,
|
total_amount: newTotal
|
||||||
[field]: numValue,
|
|
||||||
total: field !== 'vendor' ? newTotal : invoiceData.total
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
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 (
|
return (
|
||||||
|
@ -438,14 +455,9 @@ const ASFundingSection: React.FC<ASFundingSectionProps> = ({ onDataChange }) =>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
name="invoice"
|
name="invoice"
|
||||||
|
className="file-input file-input-bordered file-input-primary w-full"
|
||||||
|
onChange={handleFileUpload}
|
||||||
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"
|
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
|
required
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
|
|
@ -27,7 +27,7 @@ const EventDetailsSection: React.FC<EventDetailsSectionProps> = ({ onDataChange
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="name"
|
name="event_name"
|
||||||
className="input input-bordered w-full"
|
className="input input-bordered w-full"
|
||||||
onChange={(e) => onDataChange?.({ name: e.target.value })}
|
onChange={(e) => onDataChange?.({ name: e.target.value })}
|
||||||
required
|
required
|
||||||
|
@ -44,7 +44,7 @@ const EventDetailsSection: React.FC<EventDetailsSectionProps> = ({ onDataChange
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
name="description"
|
name="event_description"
|
||||||
className="textarea textarea-bordered h-32"
|
className="textarea textarea-bordered h-32"
|
||||||
onChange={(e) => onDataChange?.({ description: e.target.value })}
|
onChange={(e) => onDataChange?.({ description: e.target.value })}
|
||||||
required
|
required
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
interface PRSectionProps {
|
interface PRSectionProps {
|
||||||
onDataChange?: (data: any) => void;
|
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 (
|
return (
|
||||||
<div className="card bg-base-100/95 backdrop-blur-md shadow-lg">
|
<div className="card bg-base-100/95 backdrop-blur-md shadow-lg">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
|
@ -76,12 +86,26 @@ const PRSection: React.FC<PRSectionProps> = ({ onDataChange }) => {
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="checkbox checkbox-primary"
|
className="checkbox checkbox-primary"
|
||||||
|
name="flyer_type[]"
|
||||||
|
value={type.value}
|
||||||
checked={selectedTypes.includes(type.value)}
|
checked={selectedTypes.includes(type.value)}
|
||||||
onChange={() => handleTypeChange(type.value)}
|
onChange={() => handleTypeChange(type.value)}
|
||||||
/>
|
/>
|
||||||
<span className="label-text">{type.label}</span>
|
<span className="label-text">{type.label}</span>
|
||||||
</label>
|
</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>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -120,6 +144,8 @@ const PRSection: React.FC<PRSectionProps> = ({ onDataChange }) => {
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="checkbox checkbox-primary"
|
className="checkbox checkbox-primary"
|
||||||
|
name="required_logos[]"
|
||||||
|
value={logo.value}
|
||||||
checked={selectedLogos.includes(logo.value)}
|
checked={selectedLogos.includes(logo.value)}
|
||||||
onChange={() => handleLogoChange(logo.value)}
|
onChange={() => handleLogoChange(logo.value)}
|
||||||
/>
|
/>
|
||||||
|
@ -133,9 +159,7 @@ const PRSection: React.FC<PRSectionProps> = ({ onDataChange }) => {
|
||||||
<div className="form-control w-full">
|
<div className="form-control w-full">
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="label-text font-medium text-lg flex items-center gap-2">
|
<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">
|
<Icon icon="mdi:upload" className="h-5 w-5 text-primary" />
|
||||||
<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>
|
|
||||||
Upload Logo Files
|
Upload Logo Files
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
@ -145,7 +169,7 @@ const PRSection: React.FC<PRSectionProps> = ({ onDataChange }) => {
|
||||||
multiple
|
multiple
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
className="file-input file-input-bordered w-full"
|
className="file-input file-input-bordered w-full"
|
||||||
onChange={(e) => onDataChange?.({ other_logos: e.target.files })}
|
onChange={handleOtherLogosUpload}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -5,11 +5,22 @@ import InfoCard from './InfoCard';
|
||||||
import Tooltip from './Tooltip';
|
import Tooltip from './Tooltip';
|
||||||
import { tooltips, infoNotes } from './tooltips';
|
import { tooltips, infoNotes } from './tooltips';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
|
import { FileManager } from '../../../scripts/pocketbase/FileManager';
|
||||||
|
import Toast from '../universal/Toast';
|
||||||
|
import type { ASFundingSectionProps, InvoiceItem } from './ASFundingSection';
|
||||||
|
|
||||||
interface TAPSectionProps {
|
interface TAPSectionProps {
|
||||||
onDataChange?: (data: any) => void;
|
onDataChange?: (data: any) => void;
|
||||||
onASFundingChange?: (enabled: boolean) => 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 }) => {
|
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 [roomBooking, setRoomBooking] = useState<string>('');
|
||||||
const [needsASFunding, setNeedsASFunding] = useState<boolean>(false);
|
const [needsASFunding, setNeedsASFunding] = useState<boolean>(false);
|
||||||
const [needsFoodDrinks, setNeedsFoodDrinks] = 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) => {
|
const handleAttendanceChange = (value: number) => {
|
||||||
setExpectedAttendance(value);
|
setExpectedAttendance(value);
|
||||||
if (value > 100) {
|
if (value > 100) {
|
||||||
toast('Large attendance detected! Please ensure proper room capacity.', {
|
toast.custom((t) => (
|
||||||
icon: '⚠️',
|
<div className="alert alert-warning">
|
||||||
duration: 4000
|
<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 });
|
onDataChange?.({ expected_attendance: value });
|
||||||
};
|
};
|
||||||
|
@ -38,20 +54,66 @@ const TAPSection: React.FC<TAPSectionProps> = ({ onDataChange, onASFundingChange
|
||||||
setNeedsASFunding(enabled);
|
setNeedsASFunding(enabled);
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
setNeedsFoodDrinks(false);
|
setNeedsFoodDrinks(false);
|
||||||
onDataChange?.({ needs_food_drinks: false });
|
setItemizedItems([]);
|
||||||
|
onDataChange?.({ food_drinks_being_served: false });
|
||||||
}
|
}
|
||||||
onASFundingChange?.(enabled);
|
onASFundingChange?.(enabled);
|
||||||
onDataChange?.({ as_funding_required: enabled });
|
onDataChange?.({
|
||||||
|
as_funding_required: enabled,
|
||||||
toast(enabled ? 'AS Funding enabled - please fill out funding details.' : 'AS Funding disabled', {
|
itemized_items: enabled ? itemizedItems : undefined
|
||||||
icon: enabled ? '💰' : '❌',
|
|
||||||
duration: 3000
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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) => {
|
const handleFoodDrinksChange = (enabled: boolean) => {
|
||||||
setNeedsFoodDrinks(enabled);
|
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 (
|
return (
|
||||||
|
@ -97,14 +159,21 @@ const TAPSection: React.FC<TAPSectionProps> = ({ onDataChange, onASFundingChange
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
<InfoCard
|
||||||
|
title={infoNotes.funding.title}
|
||||||
|
items={infoNotes.funding.items}
|
||||||
|
type="warning"
|
||||||
|
className="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
|
name="expected_attendance"
|
||||||
className="input input-bordered w-full pl-12 transition-all duration-300 focus:ring-2 focus:ring-primary/20"
|
className="input input-bordered w-full pl-12 transition-all duration-300 focus:ring-2 focus:ring-primary/20"
|
||||||
value={expectedAttendance || ''}
|
value={expectedAttendance}
|
||||||
onChange={(e) => handleAttendanceChange(Number(e.target.value))}
|
onChange={(e) => handleAttendanceChange(parseInt(e.target.value) || 0)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -151,6 +220,27 @@ const TAPSection: React.FC<TAPSectionProps> = ({ onDataChange, onASFundingChange
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
|
@ -185,6 +275,7 @@ const TAPSection: React.FC<TAPSectionProps> = ({ onDataChange, onASFundingChange
|
||||||
className="radio radio-primary"
|
className="radio radio-primary"
|
||||||
checked={needsASFunding}
|
checked={needsASFunding}
|
||||||
onChange={() => handleASFundingChange(true)}
|
onChange={() => handleASFundingChange(true)}
|
||||||
|
value="true"
|
||||||
/>
|
/>
|
||||||
<span className="label-text">Yes, I need AS Funding</span>
|
<span className="label-text">Yes, I need AS Funding</span>
|
||||||
</label>
|
</label>
|
||||||
|
@ -195,6 +286,7 @@ const TAPSection: React.FC<TAPSectionProps> = ({ onDataChange, onASFundingChange
|
||||||
className="radio radio-primary"
|
className="radio radio-primary"
|
||||||
checked={!needsASFunding}
|
checked={!needsASFunding}
|
||||||
onChange={() => handleASFundingChange(false)}
|
onChange={() => handleASFundingChange(false)}
|
||||||
|
value="false"
|
||||||
/>
|
/>
|
||||||
<span className="label-text">No, I don't need AS Funding</span>
|
<span className="label-text">No, I don't need AS Funding</span>
|
||||||
</label>
|
</label>
|
||||||
|
@ -236,6 +328,7 @@ const TAPSection: React.FC<TAPSectionProps> = ({ onDataChange, onASFundingChange
|
||||||
className="radio radio-primary"
|
className="radio radio-primary"
|
||||||
checked={needsFoodDrinks}
|
checked={needsFoodDrinks}
|
||||||
onChange={() => handleFoodDrinksChange(true)}
|
onChange={() => handleFoodDrinksChange(true)}
|
||||||
|
value="true"
|
||||||
/>
|
/>
|
||||||
<span className="label-text">Yes, I need food/drinks</span>
|
<span className="label-text">Yes, I need food/drinks</span>
|
||||||
</label>
|
</label>
|
||||||
|
@ -246,6 +339,7 @@ const TAPSection: React.FC<TAPSectionProps> = ({ onDataChange, onASFundingChange
|
||||||
className="radio radio-primary"
|
className="radio radio-primary"
|
||||||
checked={!needsFoodDrinks}
|
checked={!needsFoodDrinks}
|
||||||
onChange={() => handleFoodDrinksChange(false)}
|
onChange={() => handleFoodDrinksChange(false)}
|
||||||
|
value="false"
|
||||||
/>
|
/>
|
||||||
<span className="label-text">No, I don't need food/drinks</span>
|
<span className="label-text">No, I don't need food/drinks</span>
|
||||||
</label>
|
</label>
|
||||||
|
@ -257,6 +351,24 @@ const TAPSection: React.FC<TAPSectionProps> = ({ onDataChange, onASFundingChange
|
||||||
</div>
|
</div>
|
||||||
</motion.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">
|
<AnimatePresence mode="popLayout">
|
||||||
{needsASFunding && (
|
{needsASFunding && (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
@ -271,7 +383,9 @@ const TAPSection: React.FC<TAPSectionProps> = ({ onDataChange, onASFundingChange
|
||||||
}}
|
}}
|
||||||
className="mt-8"
|
className="mt-8"
|
||||||
>
|
>
|
||||||
{children}
|
{children && React.cloneElement(children, {
|
||||||
|
onItemizedItemsUpdate: handleItemizedItemsUpdate
|
||||||
|
})}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
44
src/components/dashboard/universal/Toast.tsx
Normal file
44
src/components/dashboard/universal/Toast.tsx
Normal 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;
|
|
@ -42,6 +42,34 @@ export class Update {
|
||||||
return Update.instance;
|
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
|
* Update a single field in a record
|
||||||
* @param collectionName The name of the collection
|
* @param collectionName The name of the collection
|
||||||
|
@ -97,6 +125,12 @@ export class Update {
|
||||||
this.auth.setUpdating(true);
|
this.auth.setUpdating(true);
|
||||||
const pb = this.auth.getPocketBase();
|
const pb = this.auth.getPocketBase();
|
||||||
const convertedUpdates = convertLocalToUTC(updates);
|
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
|
const result = await pb
|
||||||
.collection(collectionName)
|
.collection(collectionName)
|
||||||
.update<T>(recordId, convertedUpdates);
|
.update<T>(recordId, convertedUpdates);
|
||||||
|
|
Loading…
Reference in a new issue