Starting from scratch. removed all previous attempt. Created fuel-entry.html as an MVP form - fetches vehicles via GET /api/vehicles to populate a <select>. You can enter date, odometer, full-fill checkbox, notes & tags. On submit JSON sent to POST /api/vehicles/gasrecords/add?vehickeID=<id> - Basic-Auth.

This commit is contained in:
Steve Dogiakos 2025-04-21 09:33:25 -06:00
parent 6e1e9368a6
commit 60e5bd7c44
22 changed files with 139 additions and 7826 deletions

View File

@ -1,54 +0,0 @@
# Gas Form
This repository contains the Gas Form System for the Montana Dinosaur Center. The system is designed to streamline the process of recording and managing fuel transactions for museum vehicles. The form submission links to TMDC's Lubelogger via API to track mileage and fill ups.
## Features
- Simple and user-friendly form for fuel transaction entries
- Secure storage of fuel purchase records
- Easy retrieval and reporting of fuel expenses
## Getting Started
### Prerequisites
To use or modify the Gas Form System, ensure you have:
- A modern web browser for web-based usage
- [Git](https://git-scm.com/) installed for cloning and contributing
- Basic knowledge of HTML, JavaScript, and server-side scripting (if modifications are needed)
### Installation
1. Clone the repository:
```sh
git clone https://github.com/tmdinosaurcenter/gas-form.git
cd gas-form
```
2. Open `index.html` in a web browser to access the form.
## Usage
1. Enter the required information, including:
- Vehicle information
- Mileage at time of fill up
- Fuel quantity
- Fuel cost
2. Submit the form to log the entry.
## Contribution
Contributions are welcome! To contribute:
1. Fork the repository.
2. Create a feature branch `(git checkout -b feature-branch)`.
3. Commit your changes `(git commit -m "Add new feature")`.
4. Push to the branch `(git push origin feature-branch)`.
5. Create a Pull Request.
## License
This project is licensed under the MIT License. See the [LICENSE] file for details.

View File

@ -1,27 +0,0 @@
"use server"
import { fetchVehicles, submitFillup } from "@/utils/api"
export async function getVehicles() {
try {
return await fetchVehicles()
} catch (error) {
console.error("Error fetching vehicles:", error)
throw new Error("Failed to fetch vehicles")
}
}
export async function submitGasFillup(data: {
name: string
vehicleId: string
mileage: number
gallons: number
}) {
try {
return await submitFillup(data)
} catch (error) {
console.error("Error submitting fillup:", error)
throw new Error("Failed to submit gas fillup")
}
}

View File

@ -1,56 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 300 20% 98%;
--foreground: 195 31% 13%;
--card: 300 20% 98%;
--card-foreground: 195 31% 13%;
--popover: 300 20% 98%;
--popover-foreground: 195 31% 13%;
--primary: 181 70% 46%;
--primary-foreground: 195 31% 13%;
--secondary: 191 89% 68%;
--secondary-foreground: 195 31% 13%;
--muted: 195 31% 13%;
--muted-foreground: 195 31% 45%;
--accent: 191 89% 68%;
--accent-foreground: 195 31% 13%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 195 31% 13%;
--input: 195 31% 13%;
--ring: 181 70% 46%;
--radius: 0.5rem;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
h1,
h2,
h3,
h4,
h5,
h6 {
@apply font-vollkorn;
}
}

View File

@ -1,31 +0,0 @@
import { Toaster } from "@/components/ui/toaster"
import { Vollkorn, Open_Sans } from "next/font/google"
import "./globals.css"
import type React from "react" // Import React
const vollkorn = Vollkorn({
subsets: ["latin"],
weight: ["700"],
variable: "--font-vollkorn",
})
const openSans = Open_Sans({
subsets: ["latin"],
variable: "--font-open-sans",
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={`${vollkorn.variable} ${openSans.variable} font-open-sans bg-background`}>
{children}
<Toaster />
</body>
</html>
)
}

View File

@ -1,12 +0,0 @@
"use client"
import GasFillupForm from "@/components/GasFillupForm"
export default function Page() {
return (
<main>
<GasFillupForm />
</main>
)
}

View File

@ -1,232 +0,0 @@
"use client"
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { useToast } from "@/components/ui/use-toast"
import { getVehicles, submitGasFillup } from "@/app/actions"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { FuelIcon as GasPump, Trash2 } from "lucide-react"
interface Vehicle {
id: string
name: string
}
export default function GasFillupForm() {
const { toast } = useToast()
// Form state variables
const [vehicle, setVehicle] = useState("")
const [mileage, setMileage] = useState("")
const [fuelAmount, setFuelAmount] = useState("")
const [fuelCost, setFuelCost] = useState("")
const [fuelReceipt, setFuelReceipt] = useState<File | null>(null) // Image file state
const [previewURL, setPreviewURL] = useState<string | null>(null) // Image preview state
// API-related state
const [vehicles, setVehicles] = useState<Vehicle[]>([])
const [isLoading, setIsLoading] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
// Fetch vehicles from API when the component loads
useEffect(() => {
async function loadVehicles() {
setIsLoading(true)
try {
const vehicleData = await getVehicles()
setVehicles(vehicleData)
} catch (error) {
toast({
title: "Error",
description: "Failed to load vehicles. Please try again.",
variant: "destructive",
})
} finally {
setIsLoading(false)
}
}
loadVehicles()
}, [toast])
// Allowed image formats for uploads (common phone formats)
const allowedFormats = ["image/png", "image/jpeg", "image/jpg", "image/heic", "image/heif", "image/webp"]
// Handle file selection and validation
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] || null
if (file && !allowedFormats.includes(file.type)) {
toast({
title: "Invalid file type",
description: "Please upload a valid image file (PNG, JPG, JPEG, HEIC, or WebP).",
variant: "destructive",
})
setFuelReceipt(null) // Reset file selection
setPreviewURL(null)
return
}
setFuelReceipt(file)
// Create an object URL for preview
if (file) {
const url = URL.createObjectURL(file)
setPreviewURL(url)
}
}
// Handle form submission
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)
try {
await submitGasFillup({
vehicleId: vehicle,
mileage: Number(mileage),
gallons: Number(fuelAmount),
cost: Number(fuelCost),
isFillToFull: true,
missedFuelUp: false,
file: fuelReceipt, // Attach uploaded file
})
toast({
title: "Success",
description: "Gas fillup has been recorded successfully.",
})
// Reset form state after successful submission
setVehicle("")
setMileage("")
setFuelAmount("")
setFuelCost("")
setFuelReceipt(null)
setPreviewURL(null)
} catch (error) {
toast({
title: "Error",
description: "Failed to submit gas fillup. Please try again.",
variant: "destructive",
})
} finally {
setIsSubmitting(false)
}
}
return (
<div className="min-h-screen bg-background p-4 md:p-8">
<Card className="max-w-md mx-auto border-primary shadow-lg">
{/* Form Header */}
<CardHeader className="space-y-1 text-center bg-primary text-white rounded-t-lg">
<div className="flex justify-center mb-2">
<GasPump size={32} />
</div>
<CardTitle className="text-2xl font-vollkorn">Gas Fill-up Form</CardTitle>
</CardHeader>
{/* Form Content */}
<CardContent className="p-6">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Vehicle Selection */}
<div className="space-y-2">
<Label htmlFor="vehicle" className="text-mdc-dark">Vehicle</Label>
<Select value={vehicle} onValueChange={setVehicle} disabled={isLoading || isSubmitting}>
<SelectTrigger id="vehicle" className="border-mdc-blue focus:ring-secondary">
<SelectValue placeholder={isLoading ? "Loading vehicles..." : "Select a vehicle"} />
</SelectTrigger>
<SelectContent>
{vehicles.map((v) => (
<SelectItem key={v.id} value={v.id}>{v.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Mileage Input */}
<div className="space-y-2">
<Label htmlFor="mileage" className="text-mdc-dark">Vehicle Mileage</Label>
<Input
id="mileage"
type="number"
value={mileage}
onChange={(e) => setMileage(e.target.value)}
required
min="0"
step="1"
className="border-mdc-blue focus:ring-secondary"
disabled={isSubmitting}
/>
</div>
{/* Fuel Amount Input */}
<div className="space-y-2">
<Label htmlFor="fuelAmount" className="text-mdc-dark">Fuel Amount (Gallons)</Label>
<Input
id="fuelAmount"
type="number"
value={fuelAmount}
onChange={(e) => setFuelAmount(e.target.value)}
required
min="0"
step="0.001"
className="border-mdc-blue focus:ring-secondary"
disabled={isSubmitting}
/>
</div>
{/* Fuel Cost Input */}
<div className="space-y-2">
<Label htmlFor="fuelCost" className="text-mdc-dark">Fuel Cost ($)</Label>
<Input
id="fuelCost"
type="number"
value={fuelCost}
onChange={(e) => setFuelCost(e.target.value)}
required
min="0"
step="0.01"
className="border-mdc-blue focus:ring-secondary"
disabled={isSubmitting}
/>
</div>
{/* File Upload Input */}
<div className="space-y-2">
<Label htmlFor="fuelReceipt" className="text-mdc-dark">Upload Receipt (Optional)</Label>
<Input
id="fuelReceipt"
type="file"
accept=".png, .jpg, .jpeg, .heic, .heif, .webp"
onChange={handleFileChange}
className="border-mdc-blue focus:ring-secondary"
disabled={isSubmitting}
/>
{/* Show image preview if a file is selected */}
{previewURL && (
<div className="relative mt-2">
<img src={previewURL} alt="Receipt preview" className="w-full max-h-40 object-contain rounded-md" />
<button
type="button"
className="absolute top-2 right-2 text-white bg-red-600 p-1 rounded-full"
onClick={() => { setFuelReceipt(null); setPreviewURL(null) }}
>
<Trash2 size={16} />
</button>
</div>
)}
</div>
{/* Submit Button */}
<Button type="submit" className="w-full bg-primary hover:bg-secondary text-mdc-dark font-semibold transition-colors" disabled={isSubmitting}>
{isSubmitting ? "Submitting..." : "Submit"}
</Button>
</form>
</CardContent>
</Card>
</div>
)
}

View File

@ -1,5 +0,0 @@
import { ButtonHTMLAttributes } from "react"
export function Button(props: ButtonHTMLAttributes<HTMLButtonElement>) {
return <button {...props} className="px-4 py-2 bg-primary text-white rounded-md hover:bg-secondary transition-colors" />
}

View File

@ -1,17 +0,0 @@
import * as React from "react"
export function Card({ children, className }: { children: React.ReactNode; className?: string }) {
return <div className={`border border-gray-300 rounded-lg shadow-md p-4 ${className}`}>{children}</div>
}
export function CardHeader({ children }: { children: React.ReactNode }) {
return <div className="border-b border-gray-200 pb-2 mb-4 font-semibold">{children}</div>
}
export function CardTitle({ children }: { children: React.ReactNode }) {
return <h2 className="text-xl font-bold">{children}</h2>
}
export function CardContent({ children }: { children: React.ReactNode }) {
return <div className="mt-2">{children}</div>
}

View File

@ -1,5 +0,0 @@
import { InputHTMLAttributes } from "react"
export function Input(props: InputHTMLAttributes<HTMLInputElement>) {
return <input {...props} className="border border-gray-300 p-2 rounded-md focus:outline-none focus:ring focus:ring-blue-500" />
}

View File

@ -1,3 +0,0 @@
export function Label({ htmlFor, children }: { htmlFor: string; children: React.ReactNode }) {
return <label htmlFor={htmlFor} className="block font-medium text-mdc-dark">{children}</label>
}

View File

@ -1,30 +0,0 @@
import * as React from "react"
export function Select({ children, ...props }: React.SelectHTMLAttributes<HTMLSelectElement>) {
return (
<select {...props} className="border border-gray-300 p-2 rounded-md focus:outline-none focus:ring focus:ring-blue-500">
{children}
</select>
)
}
export function SelectTrigger({ children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button {...props} className="border border-gray-300 p-2 rounded-md focus:outline-none focus:ring focus:ring-blue-500">
{children}
</button>
)
}
export function SelectContent({ children }: { children: React.ReactNode }) {
return <div className="absolute bg-white shadow-md rounded-md p-2">{children}</div>
}
export function SelectItem({ children, ...props }: React.OptionHTMLAttributes<HTMLOptionElement>) {
return <option {...props}>{children}</option>
}
export function SelectValue({ children }: { children: React.ReactNode }) {
return <span>{children}</span>
}

View File

@ -1,28 +0,0 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
export const Toast = ({ children, className }: { children: React.ReactNode; className?: string }) => {
return <div className={cn("fixed bottom-5 right-5 bg-gray-800 text-white p-3 rounded-lg shadow-lg", className)}>{children}</div>
}
export const ToastTitle = ({ children }: { children: React.ReactNode }) => {
return <strong className="block font-bold">{children}</strong>
}
export const ToastDescription = ({ children }: { children: React.ReactNode }) => {
return <p className="text-sm">{children}</p>
}
export const ToastClose = ({ onClick }: { onClick: () => void }) => {
return (
<button onClick={onClick} className="absolute top-2 right-2 text-gray-400 hover:text-gray-200">
</button>
)
}
export const ToastViewport = () => {
return <div className="fixed bottom-0 right-0 w-80 flex flex-col gap-2 p-4" />
}

View File

@ -1,17 +0,0 @@
"use client"
import { Toast, ToastClose, ToastDescription, ToastTitle, ToastViewport } from "@/components/ui/toast"
import { useToast } from "@/components/ui/use-toast"
export function Toaster() {
const { message } = useToast()
return message ? (
<Toast>
<ToastTitle>Notification</ToastTitle>
<ToastDescription>{message}</ToastDescription>
<ToastClose onClick={() => console.log("Close Toast")} />
<ToastViewport />
</Toast>
) : null
}

View File

@ -1,13 +0,0 @@
import { useState } from "react"
export function useToast() {
const [message, setMessage] = useState<string | null>(null)
const toast = ({ title, description, variant }: { title: string; description: string; variant?: "default" | "destructive" }) => {
console.log(`${variant === "destructive" ? "[ERROR]" : "[INFO]"} ${title}: ${description}`)
setMessage(`${title}: ${description}`)
setTimeout(() => setMessage(null), 3000)
}
return { toast, message }
}

139
fuel-entry.html Normal file
View File

@ -0,0 +1,139 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Add Fuel Record</title>
</head>
<body>
<h1>Add Fuel Record</h1>
<form id="fuelForm">
<label>
Vehicle:
<select id="vehicleSelect" required>
<option value="">Loading…</option>
</select>
</label>
<br /><br />
<label>
Date:
<input type="date" id="date" required>
</label>
<br /><br />
<label>
Odometer:
<input type="number" id="odometer" required>
</label>
<br /><br />
<label>
Gallons (fuelConsumed):
<input type="number" step="0.01" id="fuelConsumed" required>
</label>
<br /><br />
<label>
Cost:
<input type="number" step="0.01" id="cost" required>
</label>
<br /><br />
<label>
Fill to full:
<input type="checkbox" id="isFillToFull">
</label>
<br /><br />
<label>
Notes:
<textarea id="notes"></textarea>
</label>
<br /><br />
<label>
Tags (commaseparated):
<input type="text" id="tags">
</label>
<br /><br />
<button type="submit">Submit</button>
</form>
<div id="message"></div>
<script>
// ** CONFIGURE **
const BASE_URL = 'https://cars.dogiakos.com';
const USERNAME = 'YOUR_USERNAME';
const PASSWORD = 'YOUR_PASSWORD';
// helper to format date as MM/DD/YYYY for locale-sensitive API
function fmtDate(d) {
const [y, m, day] = d.split('-');
return `${m}/${day}/${y}`;
}
// populate vehicles dropdown
async function loadVehicles() {
const res = await fetch(`${BASE_URL}/api/vehicles`, {
headers: {
'Authorization': 'Basic ' + btoa(USERNAME + ':' + PASSWORD)
}
});
const vehicles = await res.json();
const sel = document.getElementById('vehicleSelect');
sel.innerHTML = '<option value="">Select…</option>';
vehicles.forEach(v => {
sel.innerHTML += `<option value="${v.id}">
${v.year} ${v.make} ${v.model}
</option>`;
});
}
document.getElementById('fuelForm')
.addEventListener('submit', async e => {
e.preventDefault();
const vid = document.getElementById('vehicleSelect').value;
if (!vid) return alert('Pick a vehicle');
const payload = {
date: fmtDate(document.getElementById('date').value),
odometer: Number(document.getElementById('odometer').value),
fuelConsumed: Number(document.getElementById('fuelConsumed').value),
cost: Number(document.getElementById('cost').value),
isFillToFull: document.getElementById('isFillToFull').checked,
notes: document.getElementById('notes').value,
tags: document.getElementById('tags').value,
// you can add missedFuelUp, extraFields, etc.
};
const submitUrl = `${BASE_URL}/api/vehicle/gasrecords/add?vehicleId=${vid}`;
const res = await fetch(submitUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + btoa(USERNAME + ':' + PASSWORD)
},
body: JSON.stringify(payload)
});
const msg = document.getElementById('message');
if (res.ok) {
msg.textContent = '✅ Fuel record added!';
e.target.reset();
} else {
const txt = await res.text();
msg.textContent = `❌ Error (${res.status}): ${txt}`;
}
});
// kick it off
loadVehicles();
</script>
</body>
</html>

View File

@ -1,3 +0,0 @@
export function cn(...classes: (string | undefined | null | false)[]): string {
return classes.filter(Boolean).join(" ");
}

5
next-env.d.ts vendored
View File

@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

7055
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +0,0 @@
{
"name": "gas-form",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@headlessui/react": "^1.7.17",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.3",
"lucide-react": "^0.310.0",
"next": "^15.1.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwindcss": "^3.4.1"
},
"devDependencies": {
"@types/react": "19.0.8",
"autoprefixer": "^10.4.14",
"eslint": "^8.50.0",
"eslint-config-next": "^14.0.0",
"postcss": "^8.4.31",
"shadcn-ui": "^0.9.4",
"typescript": "^5.2.2"
}
}

View File

@ -1,67 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: ["./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}"],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
// Montana Dinosaur Center colors
primary: "#23c4c8",
secondary: "#62e5f7",
background: "#f6eff6",
"mdc-dark": "#172b2d",
"mdc-blue": "#1f4e63",
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
fontFamily: {
vollkorn: ["var(--font-vollkorn)"],
"open-sans": ["var(--font-open-sans)"],
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [require("tailwindcss-animate")],
}

View File

@ -1,42 +0,0 @@
{
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"noEmit": true,
"incremental": true,
"module": "esnext",
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": ".", // Add this
"paths": {
"@/*": ["./*"],
"@/lib/*": ["lib/*"],
"@/components/*": ["components/*"],
"@/components/ui/*": ["components/ui/*"]
},
"plugins": [
{
"name": "next"
}
],
"target": "ES2017"
},
"include": [
"next-env.d.ts",
".next/types/**/*.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}

View File

@ -1,95 +0,0 @@
"use server"
let authToken: string | null = null
let tokenExpiry: number | null = null
async function getAuthToken() {
// Check if we have a valid token
if (authToken && tokenExpiry && Date.now() < tokenExpiry) {
return authToken
}
// Get new token
try {
const response = await fetch(`${process.env.LUBELOGGER_API_URL}/api/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username: process.env.LUBELOGGER_USERNAME,
password: process.env.LUBELOGGER_PASSWORD,
}),
})
if (!response.ok) {
throw new Error("Authentication failed")
}
const data = await response.json()
authToken = data.token
// Set token expiry to 1 hour from now
tokenExpiry = Date.now() + 60 * 60 * 1000
return authToken
} catch (error) {
console.error("Authentication error:", error)
throw new Error("Failed to authenticate with LubeLogger API")
}
}
export async function fetchVehicles() {
const token = await getAuthToken()
const response = await fetch(`${process.env.LUBELOGGER_API_URL}/api/vehicles`, {
headers: {
Authorization: `Bearer ${token}`,
},
})
if (!response.ok) {
throw new Error("Failed to fetch vehicles")
}
return response.json()
}
export async function submitFillup(data: {
name: string
vehicleId: string
mileage: number
gallons: number
cost: number
isFillToFull?: boolean
missedFuelUp?: boolean
notes?: string
tags?: string
}) {
const token = await getAuthToken()
const formData = new FormData()
formData.append("vehicleId", data.vehicleId)
formData.append("date", new Date().toISOString()) // Send current date/time
formData.append("odometer", data.mileage.toString())
formData.append("fuelConsumed", data.gallons.toString()) // Correct field name
formData.append("cost", data.cost.toString()) // Required by API
formData.append("isFillToFull", (data.isFillToFull ?? true).toString()) // Default: true
formData.append("missedFuelUp", (data.missedFuelUp ?? false).toString()) // Default: false
if (data.notes) formData.append("notes", data.notes)
if (data.tags) formData.append("tags", data.tags)
const response = await fetch(`${process.env.LUBELOGGER_API_URL}/api/vehicle/gasrecords/add`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
body: formData, // Use form-data instead of JSON
})
if (!response.ok) {
const error = await response.json().catch(() => ({ message: "Failed to submit fillup" }))
throw new Error(error.message)
}
return response.json()
}