Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 105 additions & 80 deletions src/views/HardwarePage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,72 +11,71 @@
<div class="row align-items-start mt-5">
<main class="col-md-12 col-lg-8 offset-lg-2">
<div class="p-4 border rounded bg-white shadow-sm main-box">

<div class="mb-4">
<input
v-model="searchQuery"
type="text"
class="form-control"
placeholder="Search hardware..."
v-model="searchQuery"
type="text"
class="form-control"
placeholder="Search hardware..."
/>
</div>

<template v-if="filteredItems.length > 0">
<div id="hardwareAccordion" class="accordion">
<div
v-for="(item, index) in filteredItems"
:key="item.name"
class="accordion-item mb-3"
<div
v-for="(item, index) in filteredItems"
:key="item.name"
class="accordion-item mb-3"
>

<h2 :id="'heading-' + getSafeId(item.name)" class="accordion-header">
<button
class="accordion-button collapsed hardware-item-header"
type="button"
data-bs-toggle="collapse"
:data-bs-target="'#collapse-' + getSafeId(item.name)"
aria-expanded="false"
:aria-controls="'collapse-' + getSafeId(item.name)"
class="accordion-button collapsed hardware-item-header"
type="button"
data-bs-toggle="collapse"
:data-bs-target="'#collapse-' + getSafeId(item.name)"
aria-expanded="false"
:aria-controls="'collapse-' + getSafeId(item.name)"
>
<span class="me-3">
<i
:class="['bi', getAvailabilityIconClass(item.availabilityStatus).icon, getAvailabilityIconClass(item.availabilityStatus).color]"
style="font-size: 1.1rem;"
:class="['bi', getAvailabilityIconClass(item.availabilityStatus).icon, getAvailabilityIconClass(item.availabilityStatus).color]"
style="font-size: 1.1rem;"
></i>
</span>
<div class="fw-bold">{{ item.name }}</div>
</button>
</h2>

<div
:id="'collapse-' + getSafeId(item.name)"
class="accordion-collapse collapse"
:aria-labelledby="'heading-' + getSafeId(item.name)"
data-bs-parent="#hardwareAccordion"
:id="'collapse-' + getSafeId(item.name)"
class="accordion-collapse collapse"
:aria-labelledby="'heading-' + getSafeId(item.name)"
data-bs-parent="#hardwareAccordion"
>
<div class="accordion-body">
<div class="d-flex align-items-start">
<div class="flex-shrink-0 me-4">
<img
<img
:src="item.image"
:alt="item.name"
class="img-thumbnail"
style="width: 150px; height: 150px; object-fit: contain;"
/>
class="img-thumbnail hardware-image"
/>
</div>

<div class="flex-grow-1">

<div class="text-end small fw-semibold mb-2">
<span :class="{'text-danger': item.isUnavailable, 'text-success': !item.isUnavailable}">
{{ item.availabilityText }}
</span>
</div>

<p class="mb-3">
{{ item.description }}
</p>

</div>
</div>
</div>
Expand All @@ -85,14 +84,15 @@
</div>
</template>
<div v-else class="text-center p-5 not-found-box">
</div>
</div>
</div>
</main>
</div>
</div>
</template>

<script>
// ... (script content remains unchanged)
import hardwareService from "@/services/hardwareService";

export default {
Expand All @@ -107,14 +107,14 @@ export default {
filteredItems() {
// If there is no search query, return the original, full list of hardware sections.
if (!this.searchQuery) {
return this.hardwareItems;
return this.hardwareItems;
}

const query = this.searchQuery.toLowerCase();
return this.hardwareItems.filter(item =>
item.name.toLowerCase().includes(query) ||
item.description.toLowerCase().includes(query)

return this.hardwareItems.filter(item =>
item.name.toLowerCase().includes(query) ||
item.description.toLowerCase().includes(query)
);
}
},
Expand All @@ -125,34 +125,35 @@ export default {
async fetchHardware() {
try {
const groups = await hardwareService.getHardware();

if (!Array.isArray(groups)) {
console.warn("Unexpected backend response, expected an array:", groups);
this.hardwareItems = [];
return;
console.warn("Unexpected backend response, expected an array:", groups);
this.hardwareItems = [];
return;
}

const allRawItems = groups.flatMap(group => {

// Assume items are nested in 'items' array, or the group itself is the item.
const itemsSource = Array.isArray(group.items) ? group.items : [group];

return itemsSource.map(p => {
const fullName = p.fullName || p.name || group.title || "Unknown Hardware";

return {
name: fullName,
description: p.description || "No description available.",
image: p.image || null,
isUnavailable: p.isUnavailable
};
});

// Assume items are nested in 'items' array, or the group itself is the item.
const itemsSource = Array.isArray(group.items) ? group.items : [group];

return itemsSource.map(p => {
const fullName = p.fullName || p.name || group.title || "Unknown Hardware";

return {
name: fullName,
description: p.description || "No description available.",
image: p.image || null,
isUnavailable: p.isUnavailable,
isNonFunctional: p.functional === false
};
});
});

this.hardwareItems = this.getAvailabilityData(allRawItems);

} catch (err) {
console.error("Failed to fetch hardware:", err);
console.error("Failed to fetch hardware:", err);
}
},
getAvailabilityData(allProducts){
Expand All @@ -168,29 +169,40 @@ export default {
image: p.image,
totalCount: 0,
unavailableCount: 0,
nonFunctionalCount: 0,
};
}
grouped[name].totalCount += 1;
if(p.isUnavailable){
grouped[name].unavailableCount += 1;
}
if (p.isNonFunctional) {
grouped[name].nonFunctionalCount += 1;
}
});

return Object.values(grouped).map(item => {
const availableCount = item.totalCount - item.unavailableCount;
// 1. Calculate the total pool of functional items
const functionalCount = item.totalCount - item.nonFunctionalCount;

// 2. Calculate the available count (Functional MINUS Unavailable)
const availableCount = functionalCount - item.unavailableCount;

let availabilityText;
let isUnavailable = false;
let availabilityStatus;

if (availableCount === 0) {
if (availableCount <= 0) {
availabilityText = "Unavailable";
isUnavailable = true;
availabilityStatus = 'none';
} else if (availableCount === item.totalCount){
availabilityText = `${availableCount} / ${item.totalCount}`;
} else if (availableCount === functionalCount){
// Status 'all' means ALL functional items are currently available
availabilityText = `${availableCount} / ${functionalCount} (Functional)`;
availabilityStatus = 'all';
}else{
availabilityText = `${availableCount} / ${item.totalCount}`;
// Status 'some' means some functional items are unavailable
availabilityText = `${availableCount} / ${functionalCount} (Functional)`;
availabilityStatus = 'some';
}

Expand All @@ -199,7 +211,9 @@ export default {
availableCount,
availabilityText,
isUnavailable,
availabilityStatus
availabilityStatus,
// Expose the functional count
functionalCount
};
});
},
Expand All @@ -219,25 +233,36 @@ export default {
}
},
getSafeId(name) {
return name ? name.toLowerCase().replace(/[^a-z0-9]+/g, '-') : '';
return name ? name.toLowerCase().replace(/[^a-z0-9]+/g, '-') : '';
}
},
};
</script>

<style scoped>
/* ... (existing styles above hardware-image) ... */

/* --- NEW CSS CLASS FOR LARGER IMAGE SIZE --- */
.hardware-image {
/* Increased from 150px to 200px */
width: 250px !important;
height: 250px !important;
object-fit: contain !important;
}
/* ------------------------------------------ */

body {
font-family: Lato, sans-serif;
color: #fff;
font-weight: 300;
font-size: 18px;
overflow-y: scroll;
overflow-x: hidden;
font-family: Lato, sans-serif;
color: #fff;
font-weight: 300;
font-size: 18px;
overflow-y: scroll;
overflow-x: hidden;
}
.container-top{
background-color: #64965d;
position: relative;
overflow: hidden;
background-color: #64965d;
position: relative;
overflow: hidden;
}
.container-fluid {
background-color: #93dda3;
Expand All @@ -256,18 +281,18 @@ body {
}
/* background-color: #64965d; */
.main-header{
margin-top: 6rem;
margin-bottom: 0;
color: #6c757d;
width: 100%;
margin-top: 6rem;
margin-bottom: 0;
color: #6c757d;
width: 100%;
}
.main-header h1 {
color: #f8f9fa;
color: #f8f9fa;
}
.header-line{
width: 60%;
margin: 0.5rem auto 0;
border-top: 3px solid #f8f9fa;
width: 60%;
margin: 0.5rem auto 0;
border-top: 3px solid #f8f9fa;
}

/* Sidebar min width */
Expand Down