This commit is contained in:
2025-03-15 19:54:35 +05:30
parent d7fa2d3cf8
commit 7c9d2ad658
9 changed files with 407 additions and 64 deletions

View File

@@ -4,13 +4,12 @@ import veb
import time import time
@['/controller/booking/create'; post] @['/controller/booking/create'; post]
pub fn (mut app App) controller_create_booking(mut ctx Context, to string, from string, name string, price int, status string, departure string) veb.Result { pub fn (mut app App) controller_create_booking(mut ctx Context, to string, from string, name string, price int, departure string) veb.Result {
fields := { fields := {
'to': to 'to': to
'from': from 'from': from
'name': name 'name': name
'price': price.str() 'price': price.str()
'status': status
'departure': departure 'departure': departure
} }
@@ -22,26 +21,93 @@ pub fn (mut app App) controller_create_booking(mut ctx Context, to string, from
} }
} }
// If any fields are empty, return field-specific error messages
if empty_fields.len > 0 {
mut response := '<script>'
// Reset all field errors first
response += 'document.querySelectorAll(".text-danger").forEach(el => el.textContent = "");'
response += 'document.querySelectorAll("input, select").forEach(el => el.classList.remove("input-error"));'
// Set error for each empty field
for field in empty_fields {
response += 'document.querySelector(".' + field + '-error").textContent = "(Required)";'
response += 'document.querySelector("[name=' + field +
']").classList.add("input-error");'
}
response += '</script>'
response += '<div class="alert alert-danger">Please fill in all required fields</div>'
return ctx.html(response)
}
user_token := ctx.get_cookie('token') or { '' } user_token := ctx.get_cookie('token') or { '' }
token := app.auth.find_token(user_token) or { return ctx.text('User not logged in') } token := app.auth.find_token(user_token) or {
return ctx.html('<div class="alert alert-danger">User not logged in. Please <a href="/login">login</a> to continue.</div>')
}
if token.user_id == 0 { if token.user_id == 0 {
return ctx.text('User not logged in') return ctx.html('<div class="alert alert-danger">User not logged in. Please <a href="/login">login</a> to continue.</div>')
} }
// Parse with specific format // Parse with specific format
departure_time := time.parse_format(departure, 'YYYY-MM-DD') or { departure_time := time.parse_format(departure, 'YYYY-MM-DD') or {
println('Parse error: ${err}') println('Parse error: ${err}')
return ctx.text('Invalid departure time format.') return ctx.html('<div class="alert alert-danger">Invalid departure date format. Please use YYYY-MM-DD format.</div>')
} }
flight := app.service_add_flight(from, to, departure_time, price) or { flight := app.service_add_flight(from, to, departure_time, price) or {
return ctx.text('Flight not added') return ctx.html('<div class="alert alert-danger">Error creating flight: ${err}</div>')
} }
app.service_add_booking(name, token.user_id, flight.id, status) or { app.service_add_booking(name, token.user_id, flight.id) or {
return ctx.text('Booking not added') return ctx.html('<div class="alert alert-danger">Error creating booking: ${err}</div>')
} }
return ctx.text('Booking created') // Return success message with HTML
success_html := '<div class="alert alert-success">
<h4>Booking created successfully!</h4>
<p>Your flight from ${from} to ${to} has been booked.</p>
<p>Passenger: ${name}</p>
<a href="/" class="btn btn-primary mt-3">Return to Home</a>
</div>'
return ctx.html(success_html)
}
@['/controller/booking/user']
pub fn (app &App) controller_get_user_bookings(mut ctx Context) veb.Result {
user_token := ctx.get_cookie('token') or { '' }
token := app.auth.find_token(user_token) or {
return ctx.html('<div class="alert alert-danger">User not logged in. Please <a href="/login">login</a> to continue.</div>')
}
if token.user_id == 0 {
return ctx.html('<div class="alert alert-danger">User not logged in. Please <a href="/login">login</a> to continue.</div>')
}
bookings := app.service_get_user_bookings(token.user_id) or {
return ctx.html('<div class="alert alert-danger">Error fetching bookings: ${err}</div>')
}
mut html := ''
for booking in bookings {
flight := app.service_get_flight(booking.flight_id) or { continue }
html += '<div class="alert alert-info">
<h4>Booking ID: ${booking.id}</h4>
<p>Flight from ${flight.from} to ${flight.to}</p>
<p>Departure: ${flight.departure.format()}</p>
<p>Passenger: ${booking.name}</p>
<p>Status: ${booking.status}</p>
<a href="/controller/booking/delete/${booking.id}" class="btn btn-danger">Delete</a>
</div>'
}
if html == '' {
html = '<div class="alert alert-info">No bookings found</div>'
}
return ctx.html(html)
} }

View File

@@ -2,12 +2,12 @@ module main
import time import time
fn (app &App) service_add_booking(name string, user_id int, flight_id int, status string) ! { fn (app &App) service_add_booking(name string, user_id int, flight_id int) ! {
booking := Booking{ booking := Booking{
name: name name: name
user_id: user_id user_id: user_id
flight_id: flight_id flight_id: flight_id
status: status status: 'conformed'
created_at: time.now() created_at: time.now()
} }
@@ -15,3 +15,11 @@ fn (app &App) service_add_booking(name string, user_id int, flight_id int, statu
insert booking into Booking insert booking into Booking
}! }!
} }
fn (app &App) service_get_user_bookings(user_id int) ![]Booking {
bookings := sql app.db {
select from Booking where user_id == user_id order by created_at desc
}!
return bookings
}

7
src/booking_view.v Normal file
View File

@@ -0,0 +1,7 @@
module main
import veb
pub fn (app &App) booking(mut ctx Context) veb.Result {
return $veb.html()
}

View File

@@ -23,3 +23,11 @@ fn (app &App) service_add_flight(from string, to string, departure time.Time, pr
return flight[0] return flight[0]
} }
fn (app &App) service_get_flight(id int) !Flight {
flight := sql app.db {
select from Flight where id == id
}!
return flight[0]
}

147
src/templates/booking.html Normal file
View File

@@ -0,0 +1,147 @@
@include 'header.html'
<div class="container mt-5">
<header class="text-center mb-4">
<h1>Create New Booking</h1>
<p>Book your next flight with us</p>
</header>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow">
<div class="card-body p-4">
<form
id="booking-form"
hx-trigger="submit"
hx-post="/controller/booking/create"
hx-target="#form-response"
hx-swap="innerHTML"
hx-indicator="#form-spinner"
>
<!-- Response container at the top of the form -->
<div id="form-response"></div>
<!-- Loading indicator -->
<div
id="form-spinner"
class="htmx-indicator"
style="display: none"
>
<div
class="spinner-border text-primary"
role="status"
>
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="from" class="form-label"
>From
<span class="text-danger from-error"></span>
</label>
<input
type="text"
class="form-control"
id="from"
name="from"
placeholder="Departure city"
/>
</div>
<div class="col-md-6">
<label for="to" class="form-label"
>To
<span class="text-danger to-error"></span>
</label>
<input
type="text"
class="form-control"
id="to"
name="to"
placeholder="Arrival city"
/>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="name" class="form-label"
>Passenger Name
<span class="text-danger name-error"></span>
</label>
<input
type="text"
class="form-control"
id="name"
name="name"
placeholder="Enter passenger name"
/>
</div>
<div class="col-md-6">
<label for="price" class="form-label"
>Price
<span
class="text-danger price-error"
></span>
</label>
<input
type="number"
class="form-control"
id="price"
name="price"
placeholder="Enter price"
/>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="departure" class="form-label">
Departure Date & Time
<span
class="text-danger departure-error"
></span>
</label>
<input
type="datetime-local"
class="form-control"
id="departure"
name="departure"
/>
</div>
<div class="col-md-6">
<label for="status" class="form-label"
>Status
<span
class="text-danger status-error"
></span>
</label>
</div>
</div>
<div class="d-grid gap-2">
<button
type="submit"
class="btn btn-primary btn-lg"
>
Create Booking
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<style>
.htmx-request .htmx-indicator {
display: inline-block !important;
}
.input-error {
border-color: #dc3545;
}
</style>
@include 'footer.html'

View File

@@ -1,17 +1,26 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Air Bookings</title> <title>Air Bookings</title>
@css '/static/css/bootstrap.css' @css '/static/css/bootstrap.css'
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css"> <link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css"
/>
@js 'https://unpkg.com/htmx.org@2.0.4'
</head> </head>
<body> <body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container"> <div class="container">
<a class="navbar-brand" href="/">Air Bookings</a> <a class="navbar-brand" href="/">Air Bookings</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"> <button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
>
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
@@ -32,4 +41,6 @@
</div> </div>
</div> </div>
</nav> </nav>
<main> <main></main>
</body>
</html>

View File

@@ -11,7 +11,10 @@
<div class="mb-4"> <div class="mb-4">
<h4>@{user.first_name} @{user.last_name}</h4> <h4>@{user.first_name} @{user.last_name}</h4>
<p><i class="bi bi-envelope"></i> @{user.email}</p> <p><i class="bi bi-envelope"></i> @{user.email}</p>
<p><i class="bi bi-gender-ambiguous"></i> @{user.gender}</p> <p>
<i class="bi bi-gender-ambiguous"></i>
@{user.gender}
</p>
</div> </div>
<div class="card mb-4"> <div class="card mb-4">
@@ -19,29 +22,71 @@
<h5>Update Profile</h5> <h5>Update Profile</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<form action="/controller/user/update" method="post"> <form
<input type="hidden" name="id" value="@{user.id}"> action="/controller/user/update"
method="post"
>
<input
type="hidden"
name="id"
value="@{user.id}"
/>
<div class="mb-3"> <div class="mb-3">
<label for="first_name" class="form-label">First Name</label> <label for="first_name" class="form-label"
<input type="text" class="form-control" id="first_name" name="first_name" value="@{user.first_name}"> >First Name</label
<div class="text-danger first_name-error field-error"></div> >
<input
type="text"
class="form-control"
id="first_name"
name="first_name"
value="@{user.first_name}"
/>
<div
class="text-danger first_name-error field-error"
></div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="last_name" class="form-label">Last Name</label> <label for="last_name" class="form-label"
<input type="text" class="form-control" id="last_name" name="last_name" value="@{user.last_name}"> >Last Name</label
<div class="text-danger last_name-error field-error"></div> >
<input
type="text"
class="form-control"
id="last_name"
name="last_name"
value="@{user.last_name}"
/>
<div
class="text-danger last_name-error field-error"
></div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="password" class="form-label">New Password (leave empty to keep current)</label> <label for="password" class="form-label"
<input type="password" class="form-control" id="password" name="password"> >New Password (leave empty to keep
<div class="text-danger password-error field-error"></div> current)</label
>
<input
type="password"
class="form-control"
id="password"
name="password"
/>
<div
class="text-danger password-error field-error"
></div>
</div> </div>
<div class="d-grid"> <div class="d-grid">
<button type="submit" class="btn btn-primary">Update Profile</button> <button
type="submit"
class="btn btn-primary"
>
Update Profile
</button>
</div> </div>
</form> </form>
</div> </div>
@@ -53,7 +98,9 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="d-grid gap-2"> <div class="d-grid gap-2">
<a href="/logout" class="btn btn-danger">Logout</a> <a href="/logout" class="btn btn-danger"
>Logout</a
>
</div> </div>
</div> </div>
</div> </div>
@@ -82,4 +129,46 @@
}); });
</script> </script>
<div class="card mt-4">
<div class="card-header">
<h5>My Bookings</h5>
</div>
<div class="card-body">
@if bookings.len > 0
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>From</th>
<th>To</th>
<th>Departure</th>
<th>Price</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@for flight in flights
<tr>
<td>@{flight.from}</td>
<td>@{flight.to}</td>
<td>@{flight.departure}</td>
<td>$@{flight.price}</td>
<td>
@if flight.bookings[0].status == 'confirmed'
<span class="badge bg-success">Confirmed</span>
@else
<span class="badge bg-warning">Pending</span>
@end
</td>
</tr>
@end
</tbody>
</table>
</div>
@else
<p class="text-center text-muted">No bookings found</p>
@end
</div>
</div>
@include 'footer.html' @include 'footer.html'

View File

@@ -33,6 +33,13 @@ pub fn (app &App) profile(mut ctx Context) veb.Result {
} }
user := app.service_get_user(token.user_id) or { return ctx.redirect('/login') } user := app.service_get_user(token.user_id) or { return ctx.redirect('/login') }
bookings := app.service_get_user_bookings(token.user_id) or { []Booking{} }
mut flights := []Flight{}
for booking in bookings {
flight := app.service_get_flight(booking.flight_id) or { continue }
flights << flight
}
return $veb.html() return $veb.html()
} }

2
v.mod
View File

@@ -1,6 +1,6 @@
Module { Module {
name: 'air_bookings' name: 'air_bookings'
description: 'An Air Plane Booking Web Application' description: 'An Flight Booking Web Application'
version: '0.0.0' version: '0.0.0'
license: 'MIT' license: 'MIT'
dependencies: [] dependencies: []