Merge branch 'main' of https://git.ieeeucsd.org/Webmaster/dev-ieeeucsd-org
This commit is contained in:
commit
11d5c4ad22
2 changed files with 198 additions and 0 deletions
|
@ -713,6 +713,28 @@ const ASFundingTab: React.FC<{ request: ExtendedEventRequest }> = ({ request })
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Copyable Invoice Format */}
|
||||||
|
<motion.div
|
||||||
|
className="bg-base-100/10 p-5 rounded-lg border border-base-100/10"
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.5 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4 mb-4">
|
||||||
|
<div className="bg-info/20 p-3 rounded-full">
|
||||||
|
<Icon icon="mdi:content-copy" className="h-6 w-6 text-info" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold">Copyable Format</h3>
|
||||||
|
<p className="text-sm text-gray-400">Copy formatted invoice data for easy sharing</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<CopyableInvoiceFormat invoiceData={invoiceData} />
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
{/* File Preview Modal */}
|
{/* File Preview Modal */}
|
||||||
<FilePreviewModal
|
<FilePreviewModal
|
||||||
isOpen={isPreviewModalOpen}
|
isOpen={isPreviewModalOpen}
|
||||||
|
@ -726,6 +748,150 @@ const ASFundingTab: React.FC<{ request: ExtendedEventRequest }> = ({ request })
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Component for copyable invoice format
|
||||||
|
const CopyableInvoiceFormat: React.FC<{ invoiceData: any }> = ({ invoiceData }) => {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [formattedText, setFormattedText] = useState<string>('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!invoiceData) {
|
||||||
|
setFormattedText('No invoice data available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse invoice data if it's a string
|
||||||
|
let parsedInvoice = null;
|
||||||
|
|
||||||
|
if (typeof invoiceData === 'string') {
|
||||||
|
try {
|
||||||
|
parsedInvoice = JSON.parse(invoiceData);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse invoice data string:', e);
|
||||||
|
setFormattedText('Invalid invoice data format');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (typeof invoiceData === 'object' && invoiceData !== null) {
|
||||||
|
parsedInvoice = invoiceData;
|
||||||
|
} else {
|
||||||
|
setFormattedText('No structured invoice data available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract items array
|
||||||
|
let items = [];
|
||||||
|
if (parsedInvoice.items && Array.isArray(parsedInvoice.items)) {
|
||||||
|
items = parsedInvoice.items;
|
||||||
|
} else if (Array.isArray(parsedInvoice)) {
|
||||||
|
items = parsedInvoice;
|
||||||
|
} else if (parsedInvoice.items && typeof parsedInvoice.items === 'object') {
|
||||||
|
items = [parsedInvoice.items]; // Wrap single item in array
|
||||||
|
} else {
|
||||||
|
// Try to find any array in the object
|
||||||
|
for (const key in parsedInvoice) {
|
||||||
|
if (Array.isArray(parsedInvoice[key])) {
|
||||||
|
items = parsedInvoice[key];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we still don't have items, check if the object itself looks like an item
|
||||||
|
if (items.length === 0 && (parsedInvoice.item || parsedInvoice.description || parsedInvoice.name)) {
|
||||||
|
items = [parsedInvoice];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the items into the required string format
|
||||||
|
const formattedItems = items.map((item: any) => {
|
||||||
|
const quantity = parseFloat(item?.quantity || 1);
|
||||||
|
const itemName = typeof item?.item === 'object'
|
||||||
|
? JSON.stringify(item.item)
|
||||||
|
: (item?.item || item?.description || item?.name || 'N/A');
|
||||||
|
const unitPrice = parseFloat(item?.unit_price || item?.unitPrice || item?.price || 0);
|
||||||
|
|
||||||
|
return `${quantity} ${itemName} x${unitPrice.toFixed(2)} each`;
|
||||||
|
}).join(' | ');
|
||||||
|
|
||||||
|
// Get tax, tip and total
|
||||||
|
const tax = parseFloat(parsedInvoice.tax || parsedInvoice.taxAmount || 0);
|
||||||
|
const tip = parseFloat(parsedInvoice.tip || parsedInvoice.tipAmount || 0);
|
||||||
|
const total = parseFloat(parsedInvoice.total || 0) ||
|
||||||
|
items.reduce((sum: number, item: any) => {
|
||||||
|
const quantity = parseFloat(item?.quantity || 1);
|
||||||
|
const price = parseFloat(item?.unit_price || item?.unitPrice || item?.price || 0);
|
||||||
|
return sum + (quantity * price);
|
||||||
|
}, 0) + tax + tip;
|
||||||
|
|
||||||
|
// Get vendor/location
|
||||||
|
const location = parsedInvoice.vendor || parsedInvoice.location || 'Unknown Vendor';
|
||||||
|
|
||||||
|
// Build the final formatted string
|
||||||
|
let result = formattedItems;
|
||||||
|
|
||||||
|
if (tax > 0) {
|
||||||
|
result += ` | Tax = ${tax.toFixed(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tip > 0) {
|
||||||
|
result += ` | Tip = ${tip.toFixed(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
result += ` | Total = ${total.toFixed(2)} from ${location}`;
|
||||||
|
|
||||||
|
setFormattedText(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error formatting invoice data:', error);
|
||||||
|
setFormattedText('Error formatting invoice data');
|
||||||
|
}
|
||||||
|
}, [invoiceData]);
|
||||||
|
|
||||||
|
const copyToClipboard = () => {
|
||||||
|
navigator.clipboard.writeText(formattedText)
|
||||||
|
.then(() => {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
toast.success('Copied to clipboard!');
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Failed to copy text: ', err);
|
||||||
|
toast.error('Failed to copy text');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-base-200/30 p-4 rounded-lg">
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<label className="text-sm font-medium text-gray-400">Formatted Invoice Data</label>
|
||||||
|
<button
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
className="btn btn-sm btn-primary gap-2"
|
||||||
|
disabled={!formattedText || formattedText.includes('No') || formattedText.includes('Error')}
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<>
|
||||||
|
<Icon icon="mdi:check" className="h-4 w-4" />
|
||||||
|
Copied!
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon icon="mdi:content-copy" className="h-4 w-4" />
|
||||||
|
Copy
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="bg-base-300/50 p-3 rounded-lg mt-2 whitespace-pre-wrap break-words text-sm">
|
||||||
|
{formattedText}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-400 mt-2">
|
||||||
|
Format: N_1 {'{item_1}'} x{'{cost_1}'} each | N_2 {'{item_2}'} x{'{cost_2}'} each | Tax = {'{tax}'} | Tip = {'{tip}'} | Total = {'{total}'} from {'{location}'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Separate component for invoice table
|
// Separate component for invoice table
|
||||||
const InvoiceTable: React.FC<{ invoiceData: any, expectedAttendance?: number }> = ({ invoiceData, expectedAttendance }) => {
|
const InvoiceTable: React.FC<{ invoiceData: any, expectedAttendance?: number }> = ({ invoiceData, expectedAttendance }) => {
|
||||||
// If no invoice data is provided, show a message
|
// If no invoice data is provided, show a message
|
||||||
|
|
|
@ -37,6 +37,38 @@ export default {
|
||||||
"radial-gradient(circle at 0% 0%, var(--tw-gradient-stops))",
|
"radial-gradient(circle at 0% 0%, var(--tw-gradient-stops))",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
daisyui: {
|
||||||
|
themes: [
|
||||||
|
{
|
||||||
|
light: {
|
||||||
|
primary: "#06659d",
|
||||||
|
secondary: "#4b92db",
|
||||||
|
accent: "#F3C135",
|
||||||
|
neutral: "#2a323c",
|
||||||
|
"base-100": "#ffffff",
|
||||||
|
"base-200": "#f8f9fa",
|
||||||
|
"base-300": "#e9ecef",
|
||||||
|
info: "#3abff8",
|
||||||
|
success: "#36d399",
|
||||||
|
warning: "#fbbd23",
|
||||||
|
error: "#f87272",
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
primary: "#88BFEC",
|
||||||
|
secondary: "#4b92db",
|
||||||
|
accent: "#F3C135",
|
||||||
|
neutral: "#191D24",
|
||||||
|
"base-100": "#0A0E1A",
|
||||||
|
"base-200": "#0d1324",
|
||||||
|
"base-300": "#1a2035",
|
||||||
|
info: "#3abff8",
|
||||||
|
success: "#36d399",
|
||||||
|
warning: "#fbbd23",
|
||||||
|
error: "#f87272",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
require("tailwindcss-motion"),
|
require("tailwindcss-motion"),
|
||||||
|
|
Loading…
Reference in a new issue