Skip to content

Commit

Permalink
Merge pull request #47 from w3bdesign/frontend
Browse files Browse the repository at this point in the history
Frontend admin functionality
  • Loading branch information
w3bdesign authored Nov 21, 2024
2 parents 8f04e86 + c42932d commit 28b67b3
Show file tree
Hide file tree
Showing 12 changed files with 783 additions and 299 deletions.
8 changes: 3 additions & 5 deletions backend/src/bookings/bookings.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,10 @@ export class BookingsController {
@Roles(UserRole.CUSTOMER, UserRole.EMPLOYEE, UserRole.ADMIN)
async cancel(
@Param("id") id: string,
@Body("reason") cancellationDto: { reason: string },
) {
if (!cancellationDto.reason) {
throw new BadRequestException("Cancellation reason is required");
}
const booking = await this.bookingsService.cancel(id, cancellationDto.reason);
// For admin cancellations, use a default reason
const reason = "Cancelled by administrator";
const booking = await this.bookingsService.cancel(id, reason);
return BookingResponseDto.fromEntity(booking);
}
}
28 changes: 24 additions & 4 deletions backend/src/bookings/bookings.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import {
Injectable,
NotFoundException,
BadRequestException,
Logger,
} from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository, MoreThan } from "typeorm";
import { Repository, MoreThan, In } from "typeorm";
import { Booking, BookingStatus } from "./entities/booking.entity";
import { CreateBookingDto } from "./dto/create-booking.dto";
import { UpdateBookingDto } from "./dto/update-booking.dto";
Expand All @@ -14,6 +15,8 @@ import { ServicesService } from "../services/services.service";

@Injectable()
export class BookingsService {
private readonly logger = new Logger(BookingsService.name);

constructor(
@InjectRepository(Booking)
private readonly bookingRepository: Repository<Booking>,
Expand Down Expand Up @@ -149,22 +152,39 @@ export class BookingsService {

async findUpcoming(): Promise<Booking[]> {
const now = new Date();
return this.bookingRepository.find({
this.logger.debug(`Finding upcoming bookings after ${now.toISOString()}`);

const bookings = await this.bookingRepository.find({
where: {
startTime: MoreThan(now),
status: BookingStatus.CONFIRMED,
status: In([BookingStatus.PENDING, BookingStatus.CONFIRMED]),
},
relations: ["customer", "employee", "employee.user", "service"],
order: { startTime: "ASC" },
});

this.logger.debug(`Found ${bookings.length} upcoming bookings`);
if (bookings.length === 0) {
this.logger.debug('No upcoming bookings found. Checking all bookings for debugging...');
const allBookings = await this.bookingRepository.find({
relations: ["customer", "employee", "employee.user", "service"],
});
this.logger.debug(`Total bookings in database: ${allBookings.length}`);
this.logger.debug('Sample booking dates:');
allBookings.slice(0, 3).forEach(booking => {
this.logger.debug(`Booking ${booking.id}: startTime=${booking.startTime}, status=${booking.status}`);
});
}

return bookings;
}

async getUpcomingCount(): Promise<number> {
const now = new Date();
return this.bookingRepository.count({
where: {
startTime: MoreThan(now),
status: BookingStatus.CONFIRMED,
status: In([BookingStatus.PENDING, BookingStatus.CONFIRMED]),
},
});
}
Expand Down
14 changes: 14 additions & 0 deletions backend/src/bookings/dto/update-booking.dto.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
import { IsOptional, IsEnum, IsString, IsDateString } from "class-validator";
import { Exclude, Transform } from "class-transformer";
import { BookingStatus } from "../entities/booking.entity";

export class UpdateBookingDto {
@Exclude()
id?: string;

@Exclude()
customerName?: string;

@Exclude()
employeeName?: string;

@Exclude()
serviceName?: string;

@IsOptional()
@IsDateString()
startTime?: string;

@IsOptional()
@IsEnum(BookingStatus)
@Transform(({ value }) => value?.toLowerCase())
status?: BookingStatus;

@IsOptional()
Expand Down
8 changes: 1 addition & 7 deletions backend/src/orders/orders.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,12 @@ export class OrdersService {
}

async findAll(): Promise<Order[]> {
const orders = await this.orderRepository.find({
return this.orderRepository.find({
relations: ['booking', 'booking.customer', 'booking.employee', 'booking.service'],
order: {
completedAt: 'DESC',
},
});

if (!orders.length) {
throw new NotFoundException('No completed orders found');
}

return orders;
}

async findOne(id: string): Promise<Order> {
Expand Down
8 changes: 4 additions & 4 deletions frontend/admin/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<div class="flex justify-between h-16">
<div class="flex">
<div class="flex-shrink-0 flex items-center">
<h1 class="text-xl font-bold text-gray-900">Admin Dashboard</h1>
<h1 class="text-xl font-bold text-gray-900">Admin Kontrollpanel</h1>
</div>
<div class="hidden sm:ml-6 sm:flex sm:space-x-8">
<router-link
Expand All @@ -16,7 +16,7 @@
$route.path === '/dashboard',
}"
>
Dashboard
Kontrollpanel
</router-link>
<router-link
to="/bookings"
Expand All @@ -26,7 +26,7 @@
$route.path.startsWith('/bookings'),
}"
>
Bookings
Bestillinger
</router-link>
</div>
</div>
Expand All @@ -35,7 +35,7 @@
@click="handleLogout"
class="ml-3 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Logout
Logg ut
</button>
</div>
</div>
Expand Down
132 changes: 132 additions & 0 deletions frontend/admin/src/components/BookingEditModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<template>
<div v-if="isOpen" class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity">
<div class="fixed inset-0 z-10 overflow-y-auto">
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<div class="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<div class="absolute right-0 top-0 hidden pr-4 pt-4 sm:block">
<button
type="button"
@click="closeModal"
class="rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
<span class="sr-only">Lukk</span>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>

<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:text-left w-full">
<h3 class="text-lg font-semibold leading-6 text-gray-900">
Rediger bestilling
</h3>
<div class="mt-4 space-y-4">
<!-- Status -->
<div>
<label class="block text-sm font-medium text-gray-700">Status</label>
<select
v-model="editedBooking.status"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
>
<option value="PENDING">Venter</option>
<option value="CONFIRMED">Bekreftet</option>
<option value="CANCELLED">Kansellert</option>
</select>
</div>

<!-- Date and Time -->
<div>
<label class="block text-sm font-medium text-gray-700">Dato og tid</label>
<input
type="datetime-local"
:value="formatDateForInput(editedBooking.startTime)"
@input="handleDateChange"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
/>
</div>

<!-- Notes -->
<div>
<label class="block text-sm font-medium text-gray-700">Notater</label>
<textarea
v-model="editedBooking.notes"
rows="3"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
></textarea>
</div>
</div>
</div>
</div>

<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button
type="button"
@click="handleSave"
class="inline-flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 sm:ml-3 sm:w-auto"
>
Lagre endringer
</button>
<button
type="button"
@click="closeModal"
class="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
>
Avbryt
</button>
</div>
</div>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue';
const props = defineProps<{
isOpen: boolean;
booking: any | null;
}>();
const emit = defineEmits<{
(e: 'close'): void;
(e: 'save', booking: any): void;
}>();
const editedBooking = ref<any>({});
watch(() => props.booking, (newBooking) => {
if (newBooking) {
editedBooking.value = {
...newBooking,
status: newBooking.status.toUpperCase()
};
}
}, { immediate: true });
const formatDateForInput = (dateString: string | undefined) => {
if (!dateString) return '';
try {
const date = new Date(dateString);
// Format: YYYY-MM-DDThh:mm
return date.toISOString().slice(0, 16);
} catch (error) {
console.error('Error formatting date for input:', error);
return '';
}
};
const handleDateChange = (event: Event) => {
const input = event.target as HTMLInputElement;
editedBooking.value.startTime = input.value;
};
const closeModal = () => {
emit('close');
};
const handleSave = () => {
emit('save', editedBooking.value);
};
</script>
Loading

0 comments on commit 28b67b3

Please sign in to comment.