added an event request form
This commit is contained in:
parent
c9ae525c8b
commit
9019a97496
9 changed files with 1681 additions and 1 deletions
323
src/components/dashboard/Officer_EventRequestForm.astro
Normal file
323
src/components/dashboard/Officer_EventRequestForm.astro
Normal file
|
@ -0,0 +1,323 @@
|
|||
---
|
||||
import { Authentication } from "../../scripts/pocketbase/Authentication";
|
||||
import { Update } from "../../scripts/pocketbase/Update";
|
||||
import { FileManager } from "../../scripts/pocketbase/FileManager";
|
||||
|
||||
// Form sections
|
||||
import PRSection from "./Officer_EventRequestForm/PRSection";
|
||||
import EventDetailsSection from "./Officer_EventRequestForm/EventDetailsSection";
|
||||
import TAPSection from "./Officer_EventRequestForm/TAPSection";
|
||||
import ASFundingSection from "./Officer_EventRequestForm/ASFundingSection";
|
||||
|
||||
const auth = Authentication.getInstance();
|
||||
const update = Update.getInstance();
|
||||
const fileManager = FileManager.getInstance();
|
||||
---
|
||||
|
||||
<div class="w-full max-w-4xl mx-auto p-6">
|
||||
<h1
|
||||
class="text-3xl font-bold mb-8 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
|
||||
>
|
||||
Event Request Form
|
||||
</h1>
|
||||
|
||||
<form id="eventRequestForm" class="space-y-8">
|
||||
<div class="card bg-base-100/95 backdrop-blur-md shadow-lg">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-xl">
|
||||
Do you need graphics from our design team?
|
||||
</h2>
|
||||
<div class="space-y-4 mt-4">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="needsGraphics"
|
||||
value="yes"
|
||||
class="radio radio-primary"
|
||||
/>
|
||||
<span class="label-text">Yes (Continue to PR Section)</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="needsGraphics"
|
||||
value="no"
|
||||
class="radio radio-primary"
|
||||
/>
|
||||
<span class="label-text">No (Skip to Event Details)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="prSection" class="hidden">
|
||||
<PRSection client:load />
|
||||
</div>
|
||||
|
||||
<div id="eventDetailsSection">
|
||||
<EventDetailsSection client:load />
|
||||
</div>
|
||||
|
||||
<div id="tapSection">
|
||||
<TAPSection
|
||||
client:load
|
||||
onDataChange={(data) => {
|
||||
// This will be handled in the client-side script
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("tap-section-change", {
|
||||
detail: data,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="asFundingSection" class="hidden">
|
||||
<ASFundingSection client:load />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-4 mt-8">
|
||||
<button
|
||||
type="button"
|
||||
id="saveAsDraft"
|
||||
class="btn btn-ghost hover:bg-base-200 gap-2"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M7.707 10.293a1 1 0 10-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L11 11.586V6h5a2 2 0 012 2v7a2 2 0 01-2 2H4a2 2 0 01-2-2V8a2 2 0 012-2h5v5.586l-1.293-1.293zM9 4a1 1 0 012 0v2H9V4z"
|
||||
></path>
|
||||
</svg>
|
||||
Save as Draft
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary gap-2 shadow-md hover:shadow-lg transition-all duration-300"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z"
|
||||
></path>
|
||||
</svg>
|
||||
Submit Request
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import { Authentication } from "../../scripts/pocketbase/Authentication";
|
||||
import { Update } from "../../scripts/pocketbase/Update";
|
||||
import { FileManager } from "../../scripts/pocketbase/FileManager";
|
||||
|
||||
// Form visibility logic
|
||||
const form = document.getElementById("eventRequestForm") as HTMLFormElement;
|
||||
const prSection = document.getElementById("prSection");
|
||||
const asFundingSection = document.getElementById("asFundingSection");
|
||||
const needsGraphicsRadios = document.getElementsByName("needsGraphics");
|
||||
|
||||
// Debug log for initial state
|
||||
console.log("Initial ASFundingSection state:", {
|
||||
element: asFundingSection,
|
||||
isHidden: asFundingSection?.classList.contains("hidden"),
|
||||
display: asFundingSection?.style.display,
|
||||
});
|
||||
|
||||
// Handle TAPSection changes
|
||||
document.addEventListener("tap-section-change", (event: any) => {
|
||||
const data = event.detail;
|
||||
console.log("TAP section change event received:", data);
|
||||
|
||||
if (asFundingSection) {
|
||||
console.log("Found ASFundingSection element");
|
||||
if (data.as_funding_required) {
|
||||
console.log("Showing AS Funding section");
|
||||
asFundingSection.classList.remove("hidden");
|
||||
asFundingSection.style.removeProperty("display");
|
||||
// Force a reflow
|
||||
void asFundingSection.offsetHeight;
|
||||
} else {
|
||||
console.log("Hiding AS Funding section");
|
||||
asFundingSection.classList.add("hidden");
|
||||
asFundingSection.style.display = "none";
|
||||
}
|
||||
|
||||
// Log the state after change
|
||||
console.log("ASFundingSection state after change:", {
|
||||
isHidden: asFundingSection.classList.contains("hidden"),
|
||||
display: asFundingSection.style.display,
|
||||
});
|
||||
} else {
|
||||
console.error("ASFundingSection element not found");
|
||||
}
|
||||
});
|
||||
|
||||
// Show/hide PR section based on radio selection
|
||||
needsGraphicsRadios.forEach((radio) => {
|
||||
radio.addEventListener("change", (e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.value === "yes" && prSection) {
|
||||
prSection.classList.remove("hidden");
|
||||
} else if (prSection) {
|
||||
prSection.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Form submission handler
|
||||
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;
|
||||
}
|
||||
|
||||
// Create the record
|
||||
const record = await update.updateFields(
|
||||
"event_request",
|
||||
data.id || "",
|
||||
data,
|
||||
);
|
||||
|
||||
// Handle file uploads if any
|
||||
const fileManager = FileManager.getInstance();
|
||||
const fileFields = ["room_booking", "invoice", "other_logos"];
|
||||
|
||||
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(
|
||||
"event_request",
|
||||
record.id,
|
||||
field,
|
||||
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);
|
||||
|
||||
// Redirect to events management page
|
||||
window.location.href = "/dashboard/events";
|
||||
} 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);
|
||||
}
|
||||
});
|
||||
|
||||
// Save as draft handler
|
||||
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 auth = Authentication.getInstance();
|
||||
const update = Update.getInstance();
|
||||
|
||||
const userId = auth.getUserId();
|
||||
if (userId) {
|
||||
data.requested_user = userId;
|
||||
}
|
||||
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,464 @@
|
|||
import React, { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import InfoCard from './InfoCard';
|
||||
import Tooltip from './Tooltip';
|
||||
import { tooltips, infoNotes } from './tooltips';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
interface InvoiceItem {
|
||||
quantity: number;
|
||||
item_name: string;
|
||||
unit_cost: number;
|
||||
}
|
||||
|
||||
interface InvoiceData {
|
||||
items: InvoiceItem[];
|
||||
tax: number;
|
||||
tip: number;
|
||||
total: number;
|
||||
vendor: string;
|
||||
}
|
||||
|
||||
interface ASFundingSectionProps {
|
||||
onDataChange?: (data: any) => void;
|
||||
}
|
||||
|
||||
const ASFundingSection: React.FC<ASFundingSectionProps> = ({ onDataChange }) => {
|
||||
const [invoiceData, setInvoiceData] = useState<InvoiceData>({
|
||||
items: [{ quantity: 0, item_name: '', unit_cost: 0 }],
|
||||
tax: 0,
|
||||
tip: 0,
|
||||
total: 0,
|
||||
vendor: ''
|
||||
});
|
||||
|
||||
const handleItemChange = (index: number, field: keyof InvoiceItem, value: string | number) => {
|
||||
const newItems = [...invoiceData.items];
|
||||
newItems[index] = { ...newItems[index], [field]: value };
|
||||
|
||||
// Calculate new total
|
||||
const itemsTotal = newItems.reduce((sum, item) => sum + (item.quantity * item.unit_cost), 0);
|
||||
const newTotal = itemsTotal + invoiceData.tax + invoiceData.tip;
|
||||
|
||||
setInvoiceData(prev => ({
|
||||
...prev,
|
||||
items: newItems,
|
||||
total: newTotal
|
||||
}));
|
||||
|
||||
// Notify parent with JSON string
|
||||
onDataChange?.({
|
||||
itemized_invoice: JSON.stringify({
|
||||
...invoiceData,
|
||||
items: newItems,
|
||||
total: newTotal
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
const addItem = () => {
|
||||
setInvoiceData(prev => ({
|
||||
...prev,
|
||||
items: [...prev.items, { quantity: 0, item_name: '', unit_cost: 0 }]
|
||||
}));
|
||||
toast('New item added', { icon: '➕' });
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
setInvoiceData(prev => ({
|
||||
...prev,
|
||||
items: newItems,
|
||||
total: newTotal
|
||||
}));
|
||||
|
||||
// Notify parent with JSON string
|
||||
onDataChange?.({
|
||||
itemized_invoice: JSON.stringify({
|
||||
...invoiceData,
|
||||
items: newItems,
|
||||
total: newTotal
|
||||
})
|
||||
});
|
||||
toast('Item removed', { icon: '🗑️' });
|
||||
}
|
||||
};
|
||||
|
||||
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 :
|
||||
field === 'tip' ?
|
||||
itemsTotal + invoiceData.tax + Number(value) :
|
||||
itemsTotal + invoiceData.tax + invoiceData.tip;
|
||||
|
||||
setInvoiceData(prev => ({
|
||||
...prev,
|
||||
[field]: numValue,
|
||||
total: field !== 'vendor' ? newTotal : prev.total
|
||||
}));
|
||||
|
||||
// Notify parent with JSON string
|
||||
onDataChange?.({
|
||||
itemized_invoice: JSON.stringify({
|
||||
...invoiceData,
|
||||
[field]: numValue,
|
||||
total: field !== 'vendor' ? newTotal : invoiceData.total
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="card bg-base-100/95 backdrop-blur-md shadow-lg hover:shadow-xl transition-all duration-300"
|
||||
>
|
||||
<div className="card-body">
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="card-title text-xl mb-6 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent flex items-center gap-2"
|
||||
>
|
||||
<Icon icon="mdi:cash" className="h-6 w-6" />
|
||||
AS Funding Details
|
||||
</motion.h2>
|
||||
|
||||
<div className="space-y-8">
|
||||
<InfoCard
|
||||
title={infoNotes.asFunding.title}
|
||||
items={infoNotes.asFunding.items}
|
||||
type="warning"
|
||||
className="mb-6"
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
className="form-control w-full"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium text-lg flex items-center gap-2">
|
||||
<Icon icon="mdi:store" className="h-5 w-5 text-primary" />
|
||||
Vendor Information
|
||||
</span>
|
||||
</label>
|
||||
<Tooltip
|
||||
title={tooltips.vendor.title}
|
||||
description={tooltips.vendor.description}
|
||||
position="left"
|
||||
>
|
||||
<div className="badge badge-primary badge-outline p-3 cursor-help">
|
||||
<Icon icon="mdi:information-outline" className="h-4 w-4" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter vendor name and location"
|
||||
className="input input-bordered w-full pl-12 transition-all duration-300 focus:ring-2 focus:ring-primary/20"
|
||||
value={invoiceData.vendor}
|
||||
onChange={(e) => handleExtraChange('vendor', e.target.value)}
|
||||
required
|
||||
/>
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-gray-400 group-hover:text-primary transition-colors duration-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="form-control w-full"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium text-lg flex items-center gap-2">
|
||||
<Icon icon="mdi:file-document-outline" className="h-5 w-5 text-primary" />
|
||||
Itemized Invoice
|
||||
</span>
|
||||
</label>
|
||||
<Tooltip
|
||||
title={tooltips.invoice.title}
|
||||
description={tooltips.invoice.description}
|
||||
position="left"
|
||||
>
|
||||
<div className="badge badge-primary badge-outline p-3 cursor-help">
|
||||
<Icon icon="mdi:information-outline" className="h-4 w-4" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<AnimatePresence mode="popLayout">
|
||||
<div className="space-y-4">
|
||||
{invoiceData.items.map((item, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="flex gap-4 items-end bg-base-200/50 p-4 rounded-lg group hover:bg-base-200 transition-colors duration-300"
|
||||
>
|
||||
<div className="form-control flex-1">
|
||||
<label className="label">
|
||||
<span className="label-text">Quantity</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
className="input input-bordered w-full transition-all duration-300 focus:ring-2 focus:ring-primary/20"
|
||||
value={item.quantity || ''}
|
||||
onChange={(e) => handleItemChange(index, 'quantity', Number(e.target.value))}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-control flex-[3]">
|
||||
<label className="label">
|
||||
<span className="label-text">Item Name</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input input-bordered w-full transition-all duration-300 focus:ring-2 focus:ring-primary/20"
|
||||
value={item.item_name}
|
||||
onChange={(e) => handleItemChange(index, 'item_name', e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-control flex-1">
|
||||
<label className="label">
|
||||
<span className="label-text">Unit Cost ($)</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
className="input input-bordered w-full pl-8 transition-all duration-300 focus:ring-2 focus:ring-primary/20"
|
||||
value={item.unit_cost || ''}
|
||||
onChange={(e) => handleItemChange(index, 'unit_cost', Number(e.target.value))}
|
||||
required
|
||||
/>
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<span className="text-gray-400">$</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<motion.button
|
||||
type="button"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
className="btn btn-ghost btn-square opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
||||
onClick={() => removeItem(index)}
|
||||
disabled={invoiceData.items.length === 1}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-error" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</AnimatePresence>
|
||||
|
||||
<motion.button
|
||||
type="button"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
className="btn btn-ghost mt-4 w-full group hover:bg-primary/10"
|
||||
onClick={addItem}
|
||||
>
|
||||
<Icon icon="mdi:plus" className="h-6 w-6 mr-2 group-hover:text-primary transition-colors duration-300" />
|
||||
Add Item
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.6 }}
|
||||
className="grid grid-cols-2 gap-4"
|
||||
>
|
||||
<div className="form-control">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium flex items-center gap-2">
|
||||
<Icon icon="mdi:percent" className="h-5 w-5 text-primary" />
|
||||
Tax ($)
|
||||
</span>
|
||||
</label>
|
||||
<Tooltip
|
||||
title={tooltips.tax.title}
|
||||
description={tooltips.tax.description}
|
||||
position="top"
|
||||
>
|
||||
<div className="badge badge-primary badge-outline p-3 cursor-help">
|
||||
<Icon icon="mdi:information-outline" className="h-4 w-4" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="relative group">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
className="input input-bordered pl-10 w-full transition-all duration-300 focus:ring-2 focus:ring-primary/20"
|
||||
value={invoiceData.tax || ''}
|
||||
onChange={(e) => handleExtraChange('tax', Number(e.target.value))}
|
||||
required
|
||||
/>
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<span className="text-gray-400 group-hover:text-primary transition-colors duration-300">$</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-control">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium flex items-center gap-2">
|
||||
<Icon icon="mdi:hand-coin" className="h-5 w-5 text-primary" />
|
||||
Tip ($)
|
||||
</span>
|
||||
</label>
|
||||
<Tooltip
|
||||
title={tooltips.tip.title}
|
||||
description={tooltips.tip.description}
|
||||
position="top"
|
||||
>
|
||||
<div className="badge badge-primary badge-outline p-3 cursor-help">
|
||||
<Icon icon="mdi:information-outline" className="h-4 w-4" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="relative group">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
className="input input-bordered pl-10 w-full transition-all duration-300 focus:ring-2 focus:ring-primary/20"
|
||||
value={invoiceData.tip || ''}
|
||||
onChange={(e) => handleExtraChange('tip', Number(e.target.value))}
|
||||
required
|
||||
/>
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<span className="text-gray-400 group-hover:text-primary transition-colors duration-300">$</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.7 }}
|
||||
className="form-control"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium text-lg flex items-center gap-2">
|
||||
<Icon icon="mdi:calculator" className="h-5 w-5 text-primary" />
|
||||
Total Amount
|
||||
</span>
|
||||
</label>
|
||||
<Tooltip
|
||||
title={tooltips.total.title}
|
||||
description={tooltips.total.description}
|
||||
position="left"
|
||||
>
|
||||
<div className="badge badge-primary badge-outline p-3 cursor-help">
|
||||
<Icon icon="mdi:information-outline" className="h-4 w-4" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="relative group">
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered pl-10 w-full font-bold bg-base-200/50 transition-all duration-300"
|
||||
value={invoiceData.total.toFixed(2)}
|
||||
disabled
|
||||
/>
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<span className="text-gray-400 group-hover:text-primary transition-colors duration-300">$</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.8 }}
|
||||
className="form-control w-full"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium text-lg flex items-center gap-2">
|
||||
<Icon icon="mdi:cloud-upload" className="h-5 w-5 text-primary" />
|
||||
Upload Invoice
|
||||
</span>
|
||||
</label>
|
||||
<Tooltip
|
||||
title={tooltips.invoice.title}
|
||||
description={tooltips.invoice.description}
|
||||
position="left"
|
||||
>
|
||||
<div className="badge badge-primary badge-outline p-3 cursor-help">
|
||||
<Icon icon="mdi:information-outline" className="h-4 w-4" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<InfoCard
|
||||
title={infoNotes.invoice.title}
|
||||
items={infoNotes.invoice.items}
|
||||
type="info"
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
<div className="relative group">
|
||||
<input
|
||||
type="file"
|
||||
name="invoice"
|
||||
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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-gray-400 group-hover:text-primary transition-colors duration-300" 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>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ASFundingSection;
|
|
@ -0,0 +1,149 @@
|
|||
import React from 'react';
|
||||
|
||||
interface EventDetailsSectionProps {
|
||||
onDataChange?: (data: any) => void;
|
||||
}
|
||||
|
||||
const EventDetailsSection: React.FC<EventDetailsSectionProps> = ({ onDataChange }) => {
|
||||
return (
|
||||
<div className="card bg-base-100/95 backdrop-blur-md shadow-lg">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title text-xl mb-6 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" 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" />
|
||||
</svg>
|
||||
Event Details
|
||||
</h2>
|
||||
|
||||
<div className="space-y-6 mt-4">
|
||||
<div className="form-control w-full">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium 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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Event Name
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
className="input input-bordered w-full"
|
||||
onChange={(e) => onDataChange?.({ name: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-control w-full">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium 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="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Event Description
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
name="description"
|
||||
className="textarea textarea-bordered h-32"
|
||||
onChange={(e) => onDataChange?.({ description: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-control w-full">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium 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="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Event Start Date
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="start_date_time"
|
||||
className="input input-bordered w-full"
|
||||
onChange={(e) => onDataChange?.({ start_date_time: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-control w-full">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium 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="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Event End Date
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="end_date_time"
|
||||
className="input input-bordered w-full"
|
||||
onChange={(e) => onDataChange?.({ end_date_time: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-control w-full">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium 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="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
Event Location
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="location"
|
||||
className="input input-bordered w-full"
|
||||
onChange={(e) => onDataChange?.({ location: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-control w-full">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium 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="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
Room Booking Status
|
||||
</span>
|
||||
</label>
|
||||
<div className="flex gap-4">
|
||||
<label className="label cursor-pointer justify-start gap-3 hover:bg-base-200/50 p-4 rounded-lg transition-colors duration-300">
|
||||
<input
|
||||
type="radio"
|
||||
name="will_or_have_room_booking"
|
||||
value="true"
|
||||
className="radio radio-primary"
|
||||
onChange={(e) => onDataChange?.({ will_or_have_room_booking: e.target.value === 'true' })}
|
||||
required
|
||||
/>
|
||||
<span className="label-text">Yes</span>
|
||||
</label>
|
||||
<label className="label cursor-pointer justify-start gap-3 hover:bg-base-200/50 p-4 rounded-lg transition-colors duration-300">
|
||||
<input
|
||||
type="radio"
|
||||
name="will_or_have_room_booking"
|
||||
value="false"
|
||||
className="radio radio-primary"
|
||||
onChange={(e) => onDataChange?.({ will_or_have_room_booking: e.target.value === 'true' })}
|
||||
required
|
||||
/>
|
||||
<span className="label-text">No</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventDetailsSection;
|
|
@ -0,0 +1,78 @@
|
|||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
interface InfoCardProps {
|
||||
title: string;
|
||||
items: readonly string[] | string[];
|
||||
type?: 'info' | 'warning' | 'success';
|
||||
icon?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultIcons = {
|
||||
info: <Icon icon="mdi:information-outline" className="text-info shrink-0 w-6 h-6" />,
|
||||
warning: <Icon icon="mdi:alert-outline" className="text-warning shrink-0 w-6 h-6" />,
|
||||
success: <Icon icon="mdi:check-circle-outline" className="text-success shrink-0 w-6 h-6" />
|
||||
};
|
||||
|
||||
const typeStyles = {
|
||||
info: 'alert-info bg-info/10',
|
||||
warning: 'alert-warning bg-warning/10',
|
||||
success: 'alert-success bg-success/10'
|
||||
};
|
||||
|
||||
export const InfoCard: React.FC<InfoCardProps> = ({
|
||||
title,
|
||||
items,
|
||||
type = 'info',
|
||||
icon,
|
||||
className = ''
|
||||
}) => {
|
||||
const listVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
show: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, x: -20 },
|
||||
show: { opacity: 1, x: 0 }
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className={`alert ${typeStyles[type]} shadow-sm ${className}`}
|
||||
>
|
||||
{icon || defaultIcons[type]}
|
||||
<div className="text-sm space-y-2">
|
||||
<p className="font-medium">{title}</p>
|
||||
<motion.ul
|
||||
className="space-y-1 ml-1"
|
||||
variants={listVariants}
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<motion.li
|
||||
key={index}
|
||||
variants={itemVariants}
|
||||
className="flex items-start gap-2"
|
||||
>
|
||||
<span className="text-base leading-6">•</span>
|
||||
<span>{item}</span>
|
||||
</motion.li>
|
||||
))}
|
||||
</motion.ul>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfoCard;
|
232
src/components/dashboard/Officer_EventRequestForm/PRSection.tsx
Normal file
232
src/components/dashboard/Officer_EventRequestForm/PRSection.tsx
Normal file
|
@ -0,0 +1,232 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
interface PRSectionProps {
|
||||
onDataChange?: (data: any) => void;
|
||||
}
|
||||
|
||||
const PRSection: React.FC<PRSectionProps> = ({ onDataChange }) => {
|
||||
const [selectedTypes, setSelectedTypes] = useState<string[]>([]);
|
||||
const [selectedLogos, setSelectedLogos] = useState<string[]>([]);
|
||||
|
||||
const flyerTypes = [
|
||||
{ value: 'digital_with_social', label: 'Digital flyer (with social media advertising: Facebook, Instagram, Discord)' },
|
||||
{ value: 'digital_no_social', label: 'Digital flyer (with NO social media advertising)' },
|
||||
{ value: 'physical_with_advertising', label: 'Physical flyer (with advertising)' },
|
||||
{ value: 'physical_no_advertising', label: 'Physical flyer (with NO advertising)' },
|
||||
{ value: 'newsletter', label: 'Newsletter (IEEE, ECE, IDEA)' },
|
||||
{ value: 'other', label: 'Other' }
|
||||
];
|
||||
|
||||
const logoOptions = [
|
||||
{ value: 'IEEE', label: 'IEEE' },
|
||||
{ value: 'AS', label: 'AS' },
|
||||
{ value: 'HKN', label: 'HKN' },
|
||||
{ value: 'TESC', label: 'TESC' },
|
||||
{ value: 'PIB', label: 'PIB' },
|
||||
{ value: 'TNT', label: 'TNT' },
|
||||
{ value: 'SWE', label: 'SWE' },
|
||||
{ value: 'OTHER', label: 'OTHER' }
|
||||
];
|
||||
|
||||
const handleTypeChange = (value: string) => {
|
||||
const newTypes = selectedTypes.includes(value)
|
||||
? selectedTypes.filter(type => type !== value)
|
||||
: [...selectedTypes, value];
|
||||
setSelectedTypes(newTypes);
|
||||
|
||||
if (onDataChange) {
|
||||
onDataChange({ flyer_type: newTypes });
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogoChange = (value: string) => {
|
||||
const newLogos = selectedLogos.includes(value)
|
||||
? selectedLogos.filter(logo => logo !== value)
|
||||
: [...selectedLogos, value];
|
||||
setSelectedLogos(newLogos);
|
||||
|
||||
if (onDataChange) {
|
||||
onDataChange({ required_logos: newLogos });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card bg-base-100/95 backdrop-blur-md shadow-lg">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title text-xl mb-6 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
PR Materials
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4 mt-4">
|
||||
<label className="form-control w-full">
|
||||
<div 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="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Type of material needed
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{flyerTypes.map(type => (
|
||||
<label key={type.value} className="label cursor-pointer justify-start gap-3 hover:bg-base-200/50 p-4 rounded-lg transition-colors duration-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox checkbox-primary"
|
||||
checked={selectedTypes.includes(type.value)}
|
||||
onChange={() => handleTypeChange(type.value)}
|
||||
/>
|
||||
<span className="label-text">{type.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{selectedTypes.length > 0 && (
|
||||
<>
|
||||
<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="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Advertising Start Date
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="flyer_advertising_start_date"
|
||||
className="input input-bordered w-full"
|
||||
onChange={(e) => onDataChange?.({ flyer_advertising_start_date: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Logos Required
|
||||
</span>
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{logoOptions.map(logo => (
|
||||
<label key={logo.value} className="label cursor-pointer justify-start gap-3 hover:bg-base-200/50 p-4 rounded-lg transition-colors duration-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox checkbox-primary"
|
||||
checked={selectedLogos.includes(logo.value)}
|
||||
onChange={() => handleLogoChange(logo.value)}
|
||||
/>
|
||||
<span className="label-text">{logo.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedLogos.includes('OTHER') && (
|
||||
<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>
|
||||
Upload Logo Files
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
name="other_logos"
|
||||
multiple
|
||||
accept="image/*"
|
||||
className="file-input file-input-bordered w-full"
|
||||
onChange={(e) => onDataChange?.({ other_logos: e.target.files })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Format Needed
|
||||
</span>
|
||||
</label>
|
||||
<select
|
||||
name="advertising_format"
|
||||
className="select select-bordered w-full"
|
||||
onChange={(e) => onDataChange?.({ advertising_format: e.target.value })}
|
||||
>
|
||||
<option value="">Select a format...</option>
|
||||
<option value="pdf">PDF</option>
|
||||
<option value="png">PNG</option>
|
||||
<option value="jpeg">JPG</option>
|
||||
<option value="does_not_matter">DOES NOT MATTER</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
Additional Specifications
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
name="flyer_additional_requests"
|
||||
className="textarea textarea-bordered h-32"
|
||||
placeholder="Color scheme, overall design, examples to consider..."
|
||||
onChange={(e) => onDataChange?.({ flyer_additional_requests: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
Photography Needed
|
||||
</span>
|
||||
</label>
|
||||
<div className="flex gap-4">
|
||||
<label className="label cursor-pointer justify-start gap-3 hover:bg-base-200/50 p-4 rounded-lg transition-colors duration-300">
|
||||
<input
|
||||
type="radio"
|
||||
name="photography_needed"
|
||||
value="true"
|
||||
className="radio radio-primary"
|
||||
onChange={(e) => onDataChange?.({ photography_needed: e.target.value === 'true' })}
|
||||
/>
|
||||
<span className="label-text">Yes</span>
|
||||
</label>
|
||||
<label className="label cursor-pointer justify-start gap-3 hover:bg-base-200/50 p-4 rounded-lg transition-colors duration-300">
|
||||
<input
|
||||
type="radio"
|
||||
name="photography_needed"
|
||||
value="false"
|
||||
className="radio radio-primary"
|
||||
onChange={(e) => onDataChange?.({ photography_needed: e.target.value === 'true' })}
|
||||
/>
|
||||
<span className="label-text">No</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PRSection;
|
262
src/components/dashboard/Officer_EventRequestForm/TAPSection.tsx
Normal file
262
src/components/dashboard/Officer_EventRequestForm/TAPSection.tsx
Normal file
|
@ -0,0 +1,262 @@
|
|||
import React, { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import InfoCard from './InfoCard';
|
||||
import Tooltip from './Tooltip';
|
||||
import { tooltips, infoNotes } from './tooltips';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
interface TAPSectionProps {
|
||||
onDataChange?: (data: any) => void;
|
||||
onASFundingChange?: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
const TAPSection: React.FC<TAPSectionProps> = ({ onDataChange, onASFundingChange }) => {
|
||||
const [expectedAttendance, setExpectedAttendance] = useState<number>(0);
|
||||
const [roomBooking, setRoomBooking] = useState<string>('');
|
||||
const [needsASFunding, setNeedsASFunding] = useState<boolean>(false);
|
||||
const [needsFoodDrinks, setNeedsFoodDrinks] = useState<boolean>(false);
|
||||
|
||||
const handleAttendanceChange = (value: number) => {
|
||||
setExpectedAttendance(value);
|
||||
if (value > 100) {
|
||||
toast('Large attendance detected! Please ensure proper room capacity.', {
|
||||
icon: '⚠️',
|
||||
duration: 4000
|
||||
});
|
||||
}
|
||||
onDataChange?.({ expected_attendance: value });
|
||||
};
|
||||
|
||||
const handleRoomBookingChange = (value: string) => {
|
||||
setRoomBooking(value);
|
||||
onDataChange?.({ room_booking: value });
|
||||
};
|
||||
|
||||
const handleASFundingChange = (enabled: boolean) => {
|
||||
console.log('AS Funding change:', enabled);
|
||||
setNeedsASFunding(enabled);
|
||||
if (!enabled) {
|
||||
setNeedsFoodDrinks(false);
|
||||
onDataChange?.({ needs_food_drinks: false });
|
||||
}
|
||||
onASFundingChange?.(enabled);
|
||||
console.log('Sending data change:', { as_funding_required: enabled });
|
||||
onDataChange?.({ as_funding_required: enabled });
|
||||
|
||||
toast(enabled ? 'AS Funding enabled - please fill out funding details.' : 'AS Funding disabled', {
|
||||
icon: enabled ? '💰' : '❌',
|
||||
duration: 3000
|
||||
});
|
||||
};
|
||||
|
||||
const handleFoodDrinksChange = (enabled: boolean) => {
|
||||
setNeedsFoodDrinks(enabled);
|
||||
onDataChange?.({ needs_food_drinks: enabled });
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="card bg-base-100/95 backdrop-blur-md shadow-lg hover:shadow-xl transition-all duration-300"
|
||||
>
|
||||
<div className="card-body">
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="card-title text-xl mb-6 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent flex items-center gap-2"
|
||||
>
|
||||
<Icon icon="mdi:clipboard-text-outline" className="h-6 w-6" />
|
||||
TAP Form
|
||||
</motion.h2>
|
||||
|
||||
<div className="space-y-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
className="form-control w-full"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium text-lg flex items-center gap-2">
|
||||
<Icon icon="mdi:account-group" className="h-5 w-5 text-primary" />
|
||||
Expected Attendance
|
||||
</span>
|
||||
</label>
|
||||
<Tooltip
|
||||
title={tooltips.attendance.title}
|
||||
description={tooltips.attendance.description}
|
||||
position="left"
|
||||
>
|
||||
<div className="badge badge-primary badge-outline p-3 cursor-help">
|
||||
<Icon icon="mdi:information-outline" className="h-4 w-4" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
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))}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="form-control w-full"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium text-lg flex items-center gap-2">
|
||||
<Icon icon="mdi:office-building-outline" className="h-5 w-5 text-primary" />
|
||||
Room Booking
|
||||
</span>
|
||||
</label>
|
||||
<Tooltip
|
||||
title={tooltips.room.title}
|
||||
description={tooltips.room.description}
|
||||
position="left"
|
||||
>
|
||||
<div className="badge badge-primary badge-outline p-3 cursor-help">
|
||||
<Icon icon="mdi:information-outline" className="h-4 w-4" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<InfoCard
|
||||
title={infoNotes.room.title}
|
||||
items={infoNotes.room.items}
|
||||
type="info"
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
<div className="relative group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter room number and building (e.g. EBU1 2315)"
|
||||
className="input input-bordered w-full pl-12 transition-all duration-300 focus:ring-2 focus:ring-primary/20"
|
||||
value={roomBooking}
|
||||
onChange={(e) => handleRoomBookingChange(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.6 }}
|
||||
className="form-control w-full"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium text-lg flex items-center gap-2">
|
||||
<Icon icon="mdi:cash" className="h-5 w-5 text-primary" />
|
||||
AS Funding
|
||||
</span>
|
||||
</label>
|
||||
<Tooltip
|
||||
title={tooltips.asFunding.title}
|
||||
description={tooltips.asFunding.description}
|
||||
position="left"
|
||||
>
|
||||
<div className="badge badge-primary badge-outline p-3 cursor-help">
|
||||
<Icon icon="mdi:information-outline" className="h-4 w-4" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="label cursor-pointer justify-start gap-4 hover:bg-base-200/50 p-4 rounded-lg transition-colors duration-300">
|
||||
<input
|
||||
type="radio"
|
||||
name="as_funding"
|
||||
className="radio radio-primary"
|
||||
checked={needsASFunding}
|
||||
onChange={() => handleASFundingChange(true)}
|
||||
/>
|
||||
<span className="label-text">Yes, I need AS Funding</span>
|
||||
</label>
|
||||
<label className="label cursor-pointer justify-start gap-4 hover:bg-base-200/50 p-4 rounded-lg transition-colors duration-300">
|
||||
<input
|
||||
type="radio"
|
||||
name="as_funding"
|
||||
className="radio radio-primary"
|
||||
checked={!needsASFunding}
|
||||
onChange={() => handleASFundingChange(false)}
|
||||
/>
|
||||
<span className="label-text">No, I don't need AS Funding</span>
|
||||
</label>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
{needsASFunding && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="form-control w-full overflow-hidden"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium text-lg flex items-center gap-2">
|
||||
<Icon icon="mdi:food" className="h-5 w-5 text-primary" />
|
||||
Food/Drinks
|
||||
</span>
|
||||
</label>
|
||||
<Tooltip
|
||||
title={tooltips.food.title}
|
||||
description={tooltips.food.description}
|
||||
position="left"
|
||||
>
|
||||
<div className="badge badge-primary badge-outline p-3 cursor-help">
|
||||
<Icon icon="mdi:information-outline" className="h-4 w-4" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="label cursor-pointer justify-start gap-4 hover:bg-base-200/50 p-4 rounded-lg transition-colors duration-300">
|
||||
<input
|
||||
type="radio"
|
||||
name="food_drinks"
|
||||
className="radio radio-primary"
|
||||
checked={needsFoodDrinks}
|
||||
onChange={() => handleFoodDrinksChange(true)}
|
||||
/>
|
||||
<span className="label-text">Yes, I need food/drinks</span>
|
||||
</label>
|
||||
<label className="label cursor-pointer justify-start gap-4 hover:bg-base-200/50 p-4 rounded-lg transition-colors duration-300">
|
||||
<input
|
||||
type="radio"
|
||||
name="food_drinks"
|
||||
className="radio radio-primary"
|
||||
checked={!needsFoodDrinks}
|
||||
onChange={() => handleFoodDrinksChange(false)}
|
||||
/>
|
||||
<span className="label-text">No, I don't need food/drinks</span>
|
||||
</label>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TAPSection;
|
|
@ -0,0 +1,66 @@
|
|||
import React from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
interface TooltipProps {
|
||||
title: string;
|
||||
description: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
position?: 'top' | 'bottom' | 'left' | 'right';
|
||||
}
|
||||
|
||||
const positionStyles = {
|
||||
top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
|
||||
bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
|
||||
left: 'right-full top-1/2 -translate-y-1/2 mr-2',
|
||||
right: 'left-full top-1/2 -translate-y-1/2 ml-2'
|
||||
};
|
||||
|
||||
const arrowStyles = {
|
||||
top: 'bottom-[-6px] left-1/2 -translate-x-1/2 border-t-base-300 border-l-transparent border-r-transparent border-b-transparent',
|
||||
bottom: 'top-[-6px] left-1/2 -translate-x-1/2 border-b-base-300 border-l-transparent border-r-transparent border-t-transparent',
|
||||
left: 'right-[-6px] top-1/2 -translate-y-1/2 border-l-base-300 border-t-transparent border-b-transparent border-r-transparent',
|
||||
right: 'left-[-6px] top-1/2 -translate-y-1/2 border-r-base-300 border-t-transparent border-b-transparent border-l-transparent'
|
||||
};
|
||||
|
||||
export const Tooltip: React.FC<TooltipProps> = ({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
className = '',
|
||||
position = 'top'
|
||||
}) => {
|
||||
const [isVisible, setIsVisible] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative inline-block ${className}`}
|
||||
onMouseEnter={() => setIsVisible(true)}
|
||||
onMouseLeave={() => setIsVisible(false)}
|
||||
onFocus={() => setIsVisible(true)}
|
||||
onBlur={() => setIsVisible(false)}
|
||||
>
|
||||
{children}
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
className={`absolute z-50 min-w-[320px] max-w-md p-4 bg-base-100 border border-base-300 rounded-lg shadow-lg ${positionStyles[position]}`}
|
||||
>
|
||||
<div className={`absolute w-0 h-0 border-4 ${arrowStyles[position]}`} />
|
||||
<div className="space-y-2">
|
||||
<p className="font-medium text-base">{title}</p>
|
||||
<p className="text-sm leading-relaxed text-base-content/80">{description}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tooltip;
|
|
@ -0,0 +1,99 @@
|
|||
export const tooltips = {
|
||||
attendance: {
|
||||
title: "Expected Attendance",
|
||||
description:
|
||||
"Enter the total number of expected attendees. This helps us plan resources and funding appropriately.",
|
||||
maxLimit: "Maximum funding is $10 per student, up to $5,000 per event.",
|
||||
eligibility:
|
||||
"Only UCSD students, staff, and faculty are eligible to attend.",
|
||||
},
|
||||
room: {
|
||||
title: "Room Booking",
|
||||
description:
|
||||
"Enter the room number and building where your event will be held. Make sure the room capacity matches your expected attendance.",
|
||||
format: "Format: Building Room# (e.g. EBU1 2315)",
|
||||
requirements: "Room must be booked through the appropriate UCSD channels.",
|
||||
},
|
||||
asFunding: {
|
||||
title: "AS Funding",
|
||||
description:
|
||||
"Associated Students can provide funding for your event. Select this option if you need financial support.",
|
||||
maxAmount: "Maximum funding varies based on event type and attendance.",
|
||||
requirements: "Must submit request at least 6 weeks before event.",
|
||||
},
|
||||
food: {
|
||||
title: "Food & Drinks",
|
||||
description:
|
||||
"Indicate if you plan to serve food or drinks at your event. This requires additional approvals and documentation.",
|
||||
requirements:
|
||||
"Must use approved vendors and follow food safety guidelines.",
|
||||
timing: "Food orders must be finalized 2 weeks before event.",
|
||||
},
|
||||
vendor: {
|
||||
title: "Vendor Information",
|
||||
description:
|
||||
"Enter the name and location of the vendor you plan to use for food/drinks.",
|
||||
requirements: "Must be an approved AS Funding vendor.",
|
||||
format: "Format: Vendor Name - Location",
|
||||
},
|
||||
invoice: {
|
||||
title: "Invoice Details",
|
||||
description: "Provide itemized details of your planned purchases.",
|
||||
requirements:
|
||||
"All items must be clearly listed with quantities and unit costs.",
|
||||
format: "Official invoices required 2 weeks before event.",
|
||||
},
|
||||
tax: {
|
||||
title: "Sales Tax",
|
||||
description: "Enter the total sales tax amount from your invoice.",
|
||||
note: "California sales tax is typically 7.75%",
|
||||
},
|
||||
tip: {
|
||||
title: "Gratuity",
|
||||
description: "Enter the tip amount if applicable.",
|
||||
note: "Maximum 15% for delivery orders.",
|
||||
},
|
||||
total: {
|
||||
title: "Total Amount",
|
||||
description: "The total cost including items, tax, and tip.",
|
||||
note: "Cannot exceed your approved funding amount.",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const infoNotes = {
|
||||
funding: {
|
||||
title: "Funding Guidelines",
|
||||
items: [
|
||||
"Events funded by programming funds may only admit UC San Diego students, staff or faculty as guests.",
|
||||
"Only UC San Diego undergraduate students may receive items funded by the Associated Students.",
|
||||
"Event funding is granted up to $10 per student, with a maximum of $5,000 per event.",
|
||||
"Submit all documentation at least 6 weeks before the event.",
|
||||
],
|
||||
},
|
||||
room: {
|
||||
title: "Room Booking Format",
|
||||
items: [
|
||||
"Use the format: Building Room# (e.g. EBU1 2315)",
|
||||
"Make sure the room capacity matches your expected attendance",
|
||||
"Book through the appropriate UCSD channels",
|
||||
"Include any special equipment needs in your request",
|
||||
],
|
||||
},
|
||||
asFunding: {
|
||||
title: "AS Funding Requirements",
|
||||
items: [
|
||||
"Please make sure the restaurant is a valid AS Funding food vendor!",
|
||||
"Make sure to include all items, prices, and additional costs.",
|
||||
"We don't recommend paying out of pocket as reimbursements can be complex.",
|
||||
"Submit all documentation at least 6 weeks before the event.",
|
||||
],
|
||||
},
|
||||
invoice: {
|
||||
title: "Invoice Requirements",
|
||||
items: [
|
||||
"Official food invoices will be required 2 weeks before the start of your event.",
|
||||
"Format: EventName_OrderLocation_DateOfEvent",
|
||||
"Example: QPWorkathon#1_PapaJohns_01/06/2025",
|
||||
],
|
||||
},
|
||||
} as const;
|
|
@ -36,6 +36,13 @@ sections:
|
|||
component: "Officer_ReimbursementManagement"
|
||||
class: "text-info hover:text-info-focus"
|
||||
|
||||
eventRequestForm:
|
||||
title: "Event Request Form"
|
||||
icon: "heroicons:document-text"
|
||||
role: "general"
|
||||
component: "Officer_EventRequestForm"
|
||||
class: "text-info hover:text-info-focus"
|
||||
|
||||
# Sponsor Menu
|
||||
sponsorDashboard:
|
||||
title: "Sponsor Dashboard"
|
||||
|
@ -74,7 +81,7 @@ categories:
|
|||
|
||||
officer:
|
||||
title: "Officer Menu"
|
||||
sections: ["eventManagement"]
|
||||
sections: ["eventManagement", "eventRequestForm"]
|
||||
role: "general"
|
||||
|
||||
executive:
|
||||
|
|
Loading…
Reference in a new issue