Division into three Core Elements
Early on, we decided to divide the project into three distinct parts. The rationale behind this separation was that it would help keep our work more organized and focused. In hindsight, this decision has proven valuable, allowing us to address specific issues through targeted debugging without impacting other parts of the application. With each component isolated, problems in one area don't affect the accessibility of others, providing clarity and making testing more efficient and manageable. This modular approach has greatly improved our workflow and overall development process.
The three Parts
Based on this reasoning, we decided to split the frontend, backend, and design processes into distinct categories, as outlined below:
Clients
The clients are the main users of MeetMate. They discover companies, book appointments and use their time schedule. These sorts of tasks can be found in every part of the development team, be it the fetching of upcoming appointments in the backend, booking appointments seamlessly in the frontend or the intuitive and user-friendly design of the client-facing interface.
This example demonstrates the API endpoint /api/user/relevantAppointments, which allows a client to fetch their upcoming appointments. Utilizing the access token system allows us to retrieve all necessary data without requiring additional values, conserving bandwith both for the user and also our servers. After retrieving the requested data from the database, the API responds with a specific HTTP response."
@GetMapping(path = "relevantAppointments")
@ResponseBody
public ResponseEntity<?> getRelevantAppointments(
@RequestHeader(name = "Authorization") String token){ //Extracting the access token from the request headers
token = token.substring(7); //Removing the "Bearer: " prefix from the access token
try {
return ResponseEntity.ok(userService.getRelevantAppointments(token));
} catch (Throwable t) {
Class<? extends Throwable> tc = t.getClass(); //Getting the type of error
//Responding with a fitting HTTP code
if (tc == EntityNotFoundException.class)
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("message: " + t.getMessage());
if (tc == IllegalAccessException.class)
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("message: " + t.getMessage());
//Fallback if an unknown error happens
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("message: " + t.getMessage());
}
}
In the frontend, we manage client-specific data through a dedicated context that provides access to appointments, companies, and user information:
type DashboardContextProps = {
user: ClientUser | undefined;
loading: boolean;
companies: { getCompanies: Company[] } | undefined;
relevantAppointments: Appointment[];
appointments: Appointment[];
refreshAppointments: () => Promise<void>;
};
export function DashboardProvider({ children }: { children: React.ReactNode }) {
// Client-specific state management
const [user, setUser] = useState<ClientUser>();
const [appointments, setAppointments] = useState<Appointment[]>([]);
// Automatic data refresh through polling
const { data: companies } = useQuery(GET_COMPANIES, {
pollInterval: 300000
});
// ... context implementation
}
Finding a fitting design for the users dashboard plays a major role in enhancing their experience when scheduling appointments. We made it simple to use so everyone can understand it quickly. When users first see the dashboard, they can find what they need without getting confused. We removed complex features and kept only the important tools that help users manage their time well.
![[Overview Scheduler presented on the Client Dashboard]](../../assets/dashboard-overview-scheduler.png)
The main part of our dashboard is the calendar. We use bright colors like magenta, yellow, and purple to show different appointments. These colors help users in two ways: First, users can see what type of appointment they have without reading all the details. Second, when users look for a specific appointment, they can find it by looking for its color instead of reading through many appointment names.
Our dashboard makes scheduling much better than before. It changes how users work with their daily schedules. The simple design helps users manage their time more easily, making it a better tool for everyone who uses it.
Companies
While company members are not all that different from clients, the companies they belong to require significantly more functionality. Companies need robust features to manage their profiles such as handling administrative tasks such as scheduling appointments or tracking performance metrics. Additionally, companies often interact with multiple users and datasets, necessitating a scalable structure to manage these relationships efficiently.
In the frontend, this is reflected in our GraphQL implementation which manages company data and member relationships. We use a combination of GraphQL queries and React Context to efficiently manage company data:
export const GET_COMPANY = gql`
query GetCompany($id: ID!) {
getCompany(id: $id) {
id
name
description
businessType
memberIds
ownerEmail
}
}
`;
type CompanyContextType = {
user: CompanyUser | undefined;
company: { getCompany: Company } | undefined;
members: User[];
appointments: Appointment[];
clients: { getClients: User[] };
};
The backend follows the same approach, allocating an entirely new set of management classes and a separate database for this entity. The significant data loads generated by companies - especially when fetching all appointments for their clients - required a shift from the PostgreSQL database, used for clients, to a faster, read-optimized, non-relational MongoDB database. This transition ensures better performance and scalability for handling high-volume queries efficiently. An instance of reading large datasets occurs during the fetching of all clients that are dealing with a company.
public ArrayList<GetResponse> getClients(String token) throws IllegalAccessException {
String email = jwtService.extractUserEmail(token);
//Getting the user and their associated company
User companyMember = userRepository.findUserByEmail(email)
.orElseThrow(() -> new EntityNotFoundException("User not found!"));
Company company = companyRepository.findCompanyById(companyMember.getAssociatedCompany())
.orElseThrow(() -> new EntityNotFoundException("Company not found"));
//Checking if the user retrieving the client information is a member of the company
if (isNotCompanyOwner(email)
&& isNotCompanyMember(company, userRepository.findUserByEmail(email)
.orElseThrow(() -> new EntityNotFoundException("User not found!"))
.getId()))
throw new IllegalAccessException("Not a company member");
ArrayList<Appointment> appointments = appointmentRepository.findAppointmentsByCompanyId(company.getId());
ArrayList<User> clients = new ArrayList<>();
//Constructing the list of clients
User client;
for (Appointment appointment : appointments) {
client = userRepository.findUserById(appointment.getClientId()).orElse(null);
if (clients.contains(client)) continue;
clients.add(client);
}
//Retrieving data of the clients
ArrayList<GetResponse> response = new ArrayList<>();
for (User user : clients) {
response.add(GetResponse.builder()
.id(user.getId())
.name(user.getName())
.email(user.getEmail())
.build());
}
return response;
}
Appointments
The third pillar of MeetMate are the appointments, the primary idea behind the whole project. At first glance, however, they might seem less prominent than one would expect. This is because their value is derived from the integration with the other components, where they play a central role in linking users, companies, and various data points. The appointments integrate seamlessly with user profiles and company data, pulling relevant information from each to create a smooth, cohesive experience. We ensure that appointments are not just standalone entities but rather dynamic components that evolve with the system’s broader functionality, enabling better organization and user interaction.
Following the concern of high amounts of records the backend has to consider queries carefully, supporting the performance with faster fetching alongside the MongoDB database and optimised queries through GraphQL. To address the significant amount of data transferred between the server and the user, it's crucial to implement query restrictions that minimize the data sent A prime example for this is the fetching of available appointments for a company, where only relevant data is retrieved, ensuring efficiency and reducing unnecessary overhead.
public ArrayList<Appointment> getAvailableAppointments(long id, Instant date) {
ArrayList<Appointment> appointments = appointmentRepository.findAppointmentsByCompanyId(id);
ArrayList<Appointment> availableAppointments = new ArrayList<>();
// Get start of current day
LocalDate today = LocalDate.now(ZoneId.systemDefault());
Instant startOfToday = today.atStartOfDay(ZoneId.systemDefault()).toInstant();
for (Appointment appointment : appointments) {
Instant appointmentTime = appointment.getFrom();
//Considering the case where no date is given the API will output all available appointments
//Else it filters out all days appointments not on the specified day
boolean isSameDay = date == null ||
LocalDate.ofInstant(date, ZoneId.systemDefault())
.equals(LocalDate.ofInstant(appointmentTime, ZoneId.systemDefault()));
if (appointment.getStatus() == AppointmentStatus.PENDING
&& appointmentTime.isAfter(startOfToday) // Compare with start of day instead
&& isSameDay)
availableAppointments.add(appointment);
}
return availableAppointments;
}
The frontend complements the backend's efficient data handling through carefully structured GraphQL queries and React components. Our GraphQL query is designed to fetch only the essential appointment data needed for display or booking:
export const GET_AVAILABLE_APPOINTMENTS = gql`
query GetAvailableAppointments($companyId: ID!, $date: String) {
getAvailableAppointments(companyId: $companyId, date: $date) {
id
from
to
companyId
clientId
description
location
Status
title
}
}
`;
This query structure ensures we receive exactly the fields needed for appointment management and display, no more and no less. The implementation in our Bookings component showcases how we handle this data with proper error management and user feedback:
function Bookings() {
const { user, companies, appointments, refreshAppointments } = useDashboardData();
// Available appointments fetching with error handling
const { data: availableSlots, error: slotsError } = useQuery(GET_AVAILABLE_APPOINTMENTS, {
variables: {
companyId: bookingState.selectedCompany,
date: formatDateToISOWithoutTime(bookingState.selectedDate)
},
skip: bookingState.selectedCompany === "" || bookingState.step !== 2,
fetchPolicy: "network-only",
onError: (error) => {
// Comprehensive error handling for different scenarios
if (error.networkError) {
setBookingState(prev => ({
...prev,
error: "Unable to connect to the server. Please check your connection."
}));
} else if (error.graphQLErrors?.length > 0) {
// Handle specific GraphQL errors
}
},
onCompleted: (data) => {
if (!Boolean(data?.getAvailableAppointments?.length)) {
setBookingState(prev => ({
...prev,
error: "No available appointments found for this date. Please try another date."
}));
}
}
});
// Display the appointments in a scheduler component
return (
<div className="mt-4 flex h-fit w-full rounded-[20px] bg-subtle p-6">
<Scheduler
openingHours={schedulerHours}
data={filteredAppointments}
handleAppointmentCancel={handleAppointmentCancel}
handleAppointmentChange={handleAppointmentChange}
/>
</div>
);
}
- Real-time data synchronization through the Apollo Client's polling mechanism
- Comprehensive error handling for different failure scenarios
- Conditional query execution based on user interaction state
- Type-safe data handling through TypeScript integration
- Efficient state management using React hooks and contexts
The Scheduler component then takes this structured data and renders it in a user-friendly interface, handling user interactions like cancellations and modifications while maintaining data consistency with the backend. This approach ensures that users receive immediate feedback while the application maintains data integrity across all operations.