diff --git a/.gitignore b/.gitignore
index 9084250..a04b6eb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,5 @@
*.aup3
-*.wav
\ No newline at end of file
+*.wav
+node_modules/
+.env
+package.json
\ No newline at end of file
diff --git a/projects/year_02/challenge_14/Challenge_14_Magazijn_App_plannen/Documentatie/documenten/Realisatie document.docx b/projects/year_02/challenge_14/Challenge_14_Magazijn_App_plannen/Documentatie/documenten/Realisatie document.docx
index f322866..8015324 100644
Binary files a/projects/year_02/challenge_14/Challenge_14_Magazijn_App_plannen/Documentatie/documenten/Realisatie document.docx and b/projects/year_02/challenge_14/Challenge_14_Magazijn_App_plannen/Documentatie/documenten/Realisatie document.docx differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/create_user.js b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/create_user.js
new file mode 100644
index 0000000..411331c
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/create_user.js differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/db/school_warehouse.items.json b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/db/school_warehouse.items.json
new file mode 100644
index 0000000..963edd0
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/db/school_warehouse.items.json
@@ -0,0 +1,912 @@
+[{
+ "_id": {
+ "$oid": "690b150035debc2b507e710e"
+ },
+ "name": {
+ "en": "Netgear Router",
+ "nl": "Netgear Router"
+ },
+ "description": {
+ "en": "Netgear WiFi router with three antennas for strong coverage and a stable connection. Suitable for everyday use, streaming, and gaming.",
+ "nl": "Netgear WiFi-router met drie antennes voor sterk bereik en een stabiele verbinding. Geschikt voor dagelijks gebruik, streamen en gamen."
+ },
+ "location": "Heerlen",
+ "quantity": 25,
+ "imageUrl": "/images/items/item-1762333952965-217359885.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T09:12:32.991Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T09:12:32.991Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b1dba35debc2b507e7185"
+ },
+ "name": {
+ "en": "HPE network-switch",
+ "nl": "HPE netwerk-switch"
+ },
+ "description": {
+ "en": "Professional network switch with multiple Ethernet ports for fast and reliable wired connections. Perfect for business networks, servers, or larger home setups.",
+ "nl": "Professionele netwerk-switch met meerdere Ethernet-poorten voor betrouwbare en snelle bekabelde verbindingen. Ideaal voor bedrijfsnetwerken, servers of grotere thuisnetwerken."
+ },
+ "location": "Maastricht",
+ "quantity": 10,
+ "imageUrl": "/images/items/item-1762336186624-692937132.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T09:49:46.631Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T09:49:46.631Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b1e4435debc2b507e71c2"
+ },
+ "name": {
+ "en": "computer monitor",
+ "nl": "computer monitor"
+ },
+ "description": {
+ "en": "Full HD monitor with a clear display and slim bezels. Great for everyday use, work, media, and gaming.",
+ "nl": "Full HD-monitor met helder beeld en dunne randen. Geschikt voor dagelijks gebruik, werk, media en gaming."
+ },
+ "location": "Sittard",
+ "quantity": 100,
+ "imageUrl": "/images/items/item-1762336324397-286054098.png",
+ "reserved": 5,
+ "createdAt": {
+ "$date": "2025-11-05T09:52:04.401Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T12:43:24.758Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b1f1935debc2b507e71c7"
+ },
+ "name": {
+ "en": "HDMI cable",
+ "nl": "HDMI kabel"
+ },
+ "description": {
+ "en": "HDMI cable with ferrite cores for interference reduction. Features gold-plated connectors for reliable HD audio/video transmission between TVs, monitors, gaming consoles, and other HDMI devices.",
+ "nl": "HDMI-kabel met ferrietkern voor storingonderdrukking. Voorzien van vergulde connectoren voor betrouwbare HD audio/video-overdracht tussen tv's, monitoren, gameconsoles en andere HDMI-apparaten."
+ },
+ "location": "Heerlen",
+ "quantity": 100,
+ "imageUrl": "/images/items/item-1762336537233-258395909.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T09:55:37.243Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T09:55:37.243Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b1f6235debc2b507e71cc"
+ },
+ "name": {
+ "en": "CyberPowerPC gaming desktop",
+ "nl": "CyberPowerPC gaming desktop"
+ },
+ "description": {
+ "en": "CyberPowerPC gaming desktop with tempered glass panel showcasing RGB lighting. Features illuminated fans, visible internal components, and comes with matching RGB gaming keyboard and mouse. Designed for high-performance gaming with eye-catching aesthetics.",
+ "nl": "CyberPowerPC gaming desktop met glazen zijpaneel en RGB-verlichting. Voorzien van verlichte ventilatoren, zichtbare interne componenten en inclusief bijpassend RGB gaming toetsenbord en muis. Ontworpen voor krachtige gaming prestaties met opvallende esthetiek."
+ },
+ "location": "Heerlen",
+ "quantity": 25,
+ "imageUrl": "/images/items/item-1762336610278-294967324.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T09:56:50.280Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T09:56:50.280Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b202a35debc2b507e71d1"
+ },
+ "name": {
+ "en": "USB-C cable",
+ "nl": "USB-C kabel"
+ },
+ "description": {
+ "en": "USB-C charging cables and adapters. Features reversible USB-C connectors for convenient plug-in from either direction. Compatible with modern smartphones, tablets, laptops, and other USB-C devices for fast charging and data transfer.",
+ "nl": "USB-C oplaadkabels en adapters. Voorzien van omkeerbare USB-C connectoren voor gemakkelijk aansluiten vanaf beide kanten. Geschikt voor moderne smartphones, tablets, laptops en andere USB-C apparaten voor snel opladen en datatransfer."
+ },
+ "location": "Maastricht",
+ "quantity": 50,
+ "imageUrl": "/images/items/item-1762336810580-276271022.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T10:00:10.583Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T10:00:10.583Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b206335debc2b507e71d6"
+ },
+ "name": {
+ "en": "DCE KLEIN",
+ "nl": "DCE KLEIN"
+ },
+ "description": {
+ "en": "DCE KLEIN network adapter with TX/RX Ethernet ports. Features power and transmission indicator LEDs for monitoring connection status. Compact design for reliable data communication and network connectivity applications.",
+ "nl": "DCE KLEIN netwerkadapter met TX/RX Ethernet-poorten. Voorzien van power- en transmissie-indicatielampjes voor monitoring van verbindingsstatus. Compact ontwerp voor betrouwbare datacommunicatie en netwerkconnectiviteit."
+ },
+ "location": "Heerlen",
+ "quantity": 5,
+ "imageUrl": "/images/items/item-1762336867904-363624133.png",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T10:01:07.917Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T10:01:07.917Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b20be35debc2b507e71db"
+ },
+ "name": {
+ "en": "HDD",
+ "nl": "HDD"
+ },
+ "description": {
+ "en": "Internal hard disk drive (HDD) with exposed mechanism showing spinning platter, read/write head actuator arm, and circuit board. Provides data storage for computers and servers with mechanical components for reading and writing digital information.",
+ "nl": "Interne harde schijf (HDD) met zichtbaar mechanisme inclusief draaiende plaat, lees/schrijfkop actuatorarm en printplaat. Biedt gegevensopslag voor computers en servers met mechanische componenten voor het lezen en schrijven van digitale informatie."
+ },
+ "location": "Sittard",
+ "quantity": 10,
+ "imageUrl": "/images/items/item-1762336958564-240058321.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T10:02:38.567Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T10:02:38.567Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b212635debc2b507e71e0"
+ },
+ "name": {
+ "en": "External Blu-ray drive",
+ "nl": "Externe Blu-ray drive"
+ },
+ "description": {
+ "en": "External Blu-ray drive with textured black finish. Features USB 3.0 and USB-C connectivity for reading and writing Blu-ray discs, DVDs, and CDs. Portable slim design for laptops and desktops without built-in optical drives.",
+ "nl": "Externe Blu-ray drive met gestructureerde zwarte afwerking. Voorzien van USB 3.0 en USB-C connectiviteit voor het lezen en schrijven van Blu-ray discs, dvd's en cd's. Draagbaar slim ontwerp voor laptops en desktops zonder ingebouwde optische drive."
+ },
+ "location": "Maastricht",
+ "quantity": 20,
+ "imageUrl": "/images/items/item-1762337062917-846000877.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T10:04:22.925Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T10:04:22.925Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b216235debc2b507e71e5"
+ },
+ "name": {
+ "en": "NETGEAR switch",
+ "nl": "NETGEAR switch"
+ },
+ "description": {
+ "en": "NETGEAR GS324TP 24-port Gigabit Ethernet PoE+ managed switch with 2 SFP ports. Features Power over Ethernet on ports 1-24 (30W max per port), status LEDs, and rack-mountable design for business networking and IP device power delivery.",
+ "nl": "NETGEAR GS324TP 24-poorts Gigabit Ethernet PoE+ managed switch met 2 SFP-poorten. Voorzien van Power over Ethernet op poorten 1-24 (30W max per poort), statusindicatoren en rack-monteerbaar ontwerp voor zakelijke netwerken en IP-apparaat stroomvoorziening."
+ },
+ "location": "Sittard",
+ "quantity": 25,
+ "imageUrl": "/images/items/item-1762337122183-65891618.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T10:05:22.218Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T10:05:22.218Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b21cf35debc2b507e71ec"
+ },
+ "name": {
+ "en": "TOSHIBA 2.5-inch HDD",
+ "nl": "TOSHIBA 2.5-inch HDD"
+ },
+ "description": {
+ "en": "TOSHIBA 2.5-inch internal hard drive with SATA interface. Designed for laptops and portable devices, offering reliable data storage in a compact form factor with mechanical spinning disk technology.",
+ "nl": "TOSHIBA 2,5-inch interne harde schijf met SATA-interface. Ontworpen voor laptops en draagbare apparaten, biedt betrouwbare gegevensopslag in een compacte vormfactor met mechanische draaiende schijf technologie."
+ },
+ "location": "Heerlen",
+ "quantity": 10,
+ "imageUrl": "/images/items/item-1762337231487-235622162.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T10:07:11.489Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T10:07:11.489Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b223135debc2b507e71f1"
+ },
+ "name": {
+ "en": "2,5-inch laptop HDD",
+ "nl": "2,5-inch laptop HDD"
+ },
+ "description": {
+ "en": "2.5-inch laptop hard drive with exposed blue circuit board showing internal components. Features SATA connector interface and visible spinning disk mechanism. Standard form factor for notebook computer data storage upgrades and replacements.",
+ "nl": "2,5-inch laptop harde schijf met zichtbare blauwe printplaat waarop interne componenten te zien zijn. Voorzien van SATA-connector interface en zichtbaar draaiend schijfmechanisme. Standaard formaat voor data-opslag upgrades en vervangingen in notebook computers."
+ },
+ "location": "Heerlen",
+ "quantity": 25,
+ "imageUrl": "/images/items/item-1762337329644-263204031.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T10:08:49.647Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T10:08:49.647Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b227f35debc2b507e71f6"
+ },
+ "name": {
+ "en": "Crucial P3 SSD",
+ "nl": "Crucial P3 SSD"
+ },
+ "description": {
+ "en": "Crucial P3 M.2 NVMe SSD with PCIe 3.0 interface. Slim M.2 2280 form factor solid-state drive offering fast read/write speeds for system boot, application loading, and data storage in compatible laptops and desktops.",
+ "nl": "Crucial P3 M.2 NVMe SSD met PCIe 3.0 interface. Slanke M.2 2280 vormfactor solid-state drive die snelle lees/schrijfsnelheden biedt voor systeemopstart, applicatie-laden en gegevensopslag in compatibele laptops en desktops."
+ },
+ "location": "Maastricht",
+ "quantity": 50,
+ "imageUrl": "/images/items/item-1762337407188-273253468.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T10:10:07.190Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T10:10:07.190Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b22cd35debc2b507e71fb"
+ },
+ "name": {
+ "en": "drawing tablet",
+ "nl": "tekentablet"
+ },
+ "description": {
+ "en": "Pen display drawing tablet with built-in screen and pressure-sensitive stylus. Features programmable express keys for workflow customization. Ideal for digital artists, illustrators, and graphic designers working with photo editing and illustration software.",
+ "nl": "Pen display tekentablet met ingebouwd scherm en drukgevoelige stylus. Voorzien van programmeerbare sneltoetsen voor workflow-aanpassing. Ideaal voor digitale kunstenaars, illustratoren en grafisch ontwerpers die werken met fotobewerking- en illustratiesoftware."
+ },
+ "location": "Sittard",
+ "quantity": 5,
+ "imageUrl": "/images/items/item-1762337485383-599696829.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T10:11:25.385Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T10:11:25.385Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b232535debc2b507e7202"
+ },
+ "name": {
+ "en": "black USB stick",
+ "nl": "zwarte USB stick"
+ },
+ "description": {
+ "en": "A black 64 GB USB 3.0 flash drive, featuring a loop for attaching to a keyring.",
+ "nl": "Een zwarte 64 GB USB 3.0-stick, voorzien van een oogje voor een sleutelhanger."
+ },
+ "location": "Maastricht",
+ "quantity": 50,
+ "imageUrl": "/images/items/item-1762337573536-112460822.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T10:12:53.538Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T10:12:53.538Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b236f35debc2b507e7207"
+ },
+ "name": {
+ "en": "HP Chromebook",
+ "nl": "HP Chromebook"
+ },
+ "description": {
+ "en": "A grey HP Chromebook laptop.",
+ "nl": "Een grijze HP Chromebook-laptop"
+ },
+ "location": "Heerlen",
+ "quantity": 15,
+ "imageUrl": "/images/items/item-1762337647152-811870729.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T10:14:07.156Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T10:14:07.156Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b26e235debc2b507e7210"
+ },
+ "name": {
+ "en": "Raspberry Pi 4 Model B",
+ "nl": "Raspberry Pi 4 Model B"
+ },
+ "description": {
+ "en": "Raspberry Pi 4 Model B Single-Board Computer. Compact and powerful microcomputer featuring a quad-core processor, multiple USB ports, dual HDMI output, and Gigabit Ethernet. Equipped with a 40-pin GPIO header for extensive hardware interfacing and customization. Ideal for hobbyists, developers, and educators working on electronics projects, media servers, or custom computing solutions.",
+ "nl": "Raspberry Pi 4 Model B Single-Board Computer. Compacte en krachtige microcomputer met een quad-core processor, meerdere USB-poorten, dubbele HDMI-uitgang en Gigabit Ethernet. Uitgerust met een 40-pins GPIO-header voor uitgebreide hardware-interfacing en aanpassingen. Ideaal voor hobbyisten, ontwikkelaars en educatieve instellingen die werken aan elektronica-projecten, mediaservers of op maat gemaakte computeroplossingen."
+ },
+ "location": "Heerlen",
+ "quantity": 15,
+ "imageUrl": "/images/items/item-1762338530678-745200924.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T10:28:50.681Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T10:28:50.681Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b272435debc2b507e7217"
+ },
+ "name": {
+ "en": "Yellow Ethernet Cables",
+ "nl": "Gele Ethernetkabels"
+ },
+ "description": {
+ "en": "Yellow Ethernet Cables. High-quality network cables with transparent RJ45 connectors, featuring color-coded internal wiring for easy verification. Equipped with a secure latch mechanism to prevent accidental disconnection. Ideal for setting up or expanding a wired home or office network, providing a reliable and visible internet connection for routers, computers, and other devices.",
+ "nl": "Gele Ethernetkabels. Hoogwaardige netwerkkabels met transparante RJ45-connectoren, voorzien van gekleurde interne bedrading voor eenvoudige verificatie. Uitgerust met een vergrendelingsmechanisme om onbedoeld loskoppelen te voorkomen. Ideaal voor het opzetten of uitbreiden van een bekabeld thuis- of kantoornetwerk, en zorgt voor een betrouwbare en zichtbare internetverbinding voor routers, computers en andere apparaten."
+ },
+ "location": "Heerlen",
+ "quantity": 100,
+ "imageUrl": "/images/items/item-1762338596125-667343123.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T10:29:56.127Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T10:29:56.127Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b278c35debc2b507e721c"
+ },
+ "name": {
+ "en": "Black Ethernet Cables with Red Connectors",
+ "nl": "Zwarte Ethernetkabels met Rode Connectoren"
+ },
+ "description": {
+ "en": "Black Ethernet Cables with Red Connectors. High-performance network cables featuring a durable black sheath and transparent red-tinted RJ45 connectors for easy identification. Designed with a secure latch to ensure a stable and reliable connection. Ideal for organizing complex network setups or color-coding connections in data centers, offices, or home networks.",
+ "nl": "Zwarte Ethernetkabels met Rode Connectoren. Hoogwaardige netwerkkabels met een duurzame zwarte mantel en transparante, roodgetinte RJ45-connectoren voor eenvoudige herkenning. Ontworpen met een vergrendelingsclip om een stabiele en betrouwbare verbinding te garanderen. Ideaal voor het organiseren van complexe netwerkopstellingen of het kleurcoderen van verbindingen in datacenters, kantoren of thuisnetwerken."
+ },
+ "location": "Maastricht",
+ "quantity": 100,
+ "imageUrl": "/images/items/item-1762338700058-216183679.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T10:31:40.060Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T10:31:40.060Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b27d235debc2b507e7223"
+ },
+ "name": {
+ "en": "Wired Black Keyboard",
+ "nl": "Bekabeld Zwart Toetsenbord"
+ },
+ "description": {
+ "en": "Wired Black Keyboard. A standard layout keyboard featuring a full set of keys, including a numeric keypad, function keys, and arrow keys for comprehensive control. Designed with a simple, durable black chassis and a reliable wired connection. Ideal for office work, daily computing tasks, and any user needing a straightforward, no-frills input device.",
+ "nl": "Bekabeld Zwart Toetsenbord. Een toetsenbord met standaardindeling, uitgerust met een volledige set toetsen, inclusief numeriek toetsenblok, functietoetsen en pijltoetsen voor uitgebreide controle. Ontworpen met een eenvoudige, duurzame zwarte behuizing en een betrouwbare bekabelde verbinding. Ideaal voor kantoorwerk, dagelijkse computertaken en voor elke gebruiker die een eenvoudig en functioneel invoerapparaat nodig heeft."
+ },
+ "location": "Sittard",
+ "quantity": 50,
+ "imageUrl": "/images/items/item-1762338770859-626525192.png",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T10:32:50.862Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T10:32:50.862Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b2a0935debc2b507e723a"
+ },
+ "name": {
+ "en": "VGA Videocable",
+ "nl": "VGA Videokabel"
+ },
+ "description": {
+ "en": "VGA Video Cable. A standard VGA cable featuring a durable D-sub 15-pin connector with a blue housing on each end. Designed for reliable analog video transmission between a computer and a monitor or projector. Ideal for connecting legacy displays, office equipment, and for presentations in environments that still use VGA ports.",
+ "nl": "VGA Videokabel. Een standaard VGA-kabel met een duurzame D-sub 15-pins connector met een blauwe behuizing aan elke kant. Ontworpen voor betrouwbare analoge videotransmissie tussen een computer en een monitor of projector. Ideaal voor het aansluiten van oudere displays, kantoorequipment en voor presentaties in omgevingen die nog steeds VGA-poorten gebruiken."
+ },
+ "location": "Maastricht",
+ "quantity": 25,
+ "imageUrl": "/images/items/item-1762339337231-860786354.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T10:42:17.233Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T10:42:17.233Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b2a4135debc2b507e723f"
+ },
+ "name": {
+ "en": "Red Ethernet Cable.",
+ "nl": "Rode Ethernetkabel."
+ },
+ "description": {
+ "en": "Red Ethernet Cable. A high-performance network cable with a vibrant red sheath and a transparent RJ45 connector for easy identification. Designed for a secure, high-speed wired connection. Ideal for color-coding network ports in a server rack, office, or home setup to simplify cable management and troubleshooting.",
+ "nl": "Rode Ethernetkabel. Een hoogwaardige netwerkkabel met een opvallend rode mantel en een transparante RJ45-connector voor eenvoudige herkenning. Ontworpen voor een veilige, snelle bekabelde verbinding. Ideaal voor het kleurcoderen van netwerkpoorten in een serverrack, kantoor of thuisopstelling om kabelbeheer en probleemoplossing te vereenvoudigen."
+ },
+ "location": "Maastricht",
+ "quantity": 25,
+ "imageUrl": "/images/items/item-1762339393022-389531031.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T10:43:13.024Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T10:43:13.024Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b3a3f35debc2b507e7292"
+ },
+ "name": {
+ "en": "Blue CAT6 Ethernet Cable.",
+ "nl": "Blauwe CAT6 Ethernetkabel."
+ },
+ "description": {
+ "en": "Blue Coiled CAT6 Ethernet Cable. A retractable CAT6 network cable in a vibrant blue, designed to stretch and recoil for easy storage and reduced clutter. Certified for high-speed performance with clear \"CAT6\" and \"PATCH CABLE\" markings. Ideal for temporary connections, travel, or neat desk setups where a standard long cable is impractical.",
+ "nl": "Blauwe (Opgerolde) CAT6 Ethernetkabel. Een intrekbare CAT6-netwerkkabel in een helderblauwe kleur, ontworpen om uit te rekken en terug te veren voor eenvoudige opberging en minder kabelwarboel. Gecertificeerd voor hoge snelheden met duidelijke \"CAT6\" en \"PATCH CABLE\" markeringen. Ideaal voor tijdelijke verbindingen, reizen of nette bureauopstellingen waar een standaard lange kabel onpraktisch is."
+ },
+ "location": "Maastricht",
+ "quantity": 15,
+ "imageUrl": "/images/items/item-1762343487683-425439694.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T11:51:27.686Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T11:51:27.686Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b3aae35debc2b507e7297"
+ },
+ "name": {
+ "en": "Orange Flat Ethernet Cable.",
+ "nl": "Oranje Platte Ethernetkabel."
+ },
+ "description": {
+ "en": "Orange Flat Ethernet Cable. A slim, flat-profile network cable in a bright orange, designed to run under carpets or along walls without creating a trip hazard. Features a durable construction and standard RJ45 connectors. Ideal for home theater setups, office cubicles, and any situation requiring a discreet, easily concealable network connection.",
+ "nl": "Oranje Platte Ethernetkabel. Een slanke netwerkkabel met een plat profiel in een helderoranje kleur, ontworpen om onder tapijten of langs muren te leggen zonder struikelgevaar te veroorzaken. Voorzien van een duurzame constructie en standaard RJ45-connectoren. Ideaal voor home theater-opstellingen, kantoorkubussen en elke situatie waar een discrete, makkelijk weg te werken netwerkverbinding nodig is."
+ },
+ "location": "Sittard",
+ "quantity": 20,
+ "imageUrl": "/images/items/item-1762343598522-590746235.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T11:53:18.525Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T11:53:18.525Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b3b0a35debc2b507e72a0"
+ },
+ "name": {
+ "en": "Trust Wireless Mouse.",
+ "nl": "Trust Draadloze Muis."
+ },
+ "description": {
+ "en": "Trust Wireless Mouse. Ergonomic wireless mouse featuring a sleek, dark gray and black design for comfortable handling. Equipped with a scroll wheel and side buttons for enhanced navigation and productivity. Ideal for everyday office tasks, general computing, and users seeking a reliable, cable-free input solution for their desktop or laptop.",
+ "nl": "Trust Draadloze Muis. Ergonomische draadloze muis met een strak, donkergrijs en zwart ontwerp voor comfortabel gebruik. Uitgerust met een scrollwiel en zijknoppen voor verbeterde navigatie en productiviteit. Ideaal voor dagelijkse kantoortaken, algemeen computergebruik en voor gebruikers die een betrouwbare, draadloze invoeroplossing zoeken voor hun desktop of laptop."
+ },
+ "location": "Heerlen",
+ "quantity": 50,
+ "imageUrl": "/images/items/item-1762343690686-711501396.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T11:54:50.688Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T11:54:50.688Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b405a35debc2b507e72b9"
+ },
+ "name": {
+ "en": "Laptop Power Adapter.",
+ "nl": "Laptopvoeding."
+ },
+ "description": {
+ "en": "Laptop Power Adapter. Black laptop power adapter featuring a compact design with a labeled surface displaying specifications and identification marks. Comes complete with a power cable equipped with a three-prong plug on one end and a cylindrical connector on the other. Ideal for powering laptops, replacing lost or damaged adapters, and users needing a reliable power solution for their computing devices.",
+ "nl": "Laptopvoeding. Zwarte laptopvoeding met een compact ontwerp en een gelabeld oppervlak dat specificaties en identificatiemerken weergeeft. Wordt geleverd met een stroomkabel die is uitgerust met een drijfstekker aan de ene kant en een cilindrische connector aan de andere kant. Ideaal voor het voeden van laptops, het vervangen van verloren of beschadigde adapters en voor gebruikers die een betrouwbare stroomoplossing nodig hebben voor hun computerapparatuur."
+ },
+ "location": "Sittard",
+ "quantity": 15,
+ "imageUrl": "/images/items/item-1762345050512-670926874.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T12:17:30.515Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T12:17:30.515Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b40fd35debc2b507e72c6"
+ },
+ "name": {
+ "en": "Laptop Power Adapter.",
+ "nl": "Laptopvoeding."
+ },
+ "description": {
+ "en": "Laptop Power Adapter. Black power adapter featuring a compact design with a labeled surface displaying various certification marks (such as CE, FCC) and technical specifications. Comes with a black power cable, neatly secured with a strap, terminating in a small cylindrical connector. Ideal for powering laptops, replacing lost or damaged adapters, and users needing a reliable power solution for their computing devices.",
+ "nl": "Laptopvoeding. Zwarte voeding met een compact ontwerp en een gelabeld oppervlak dat diverse certificeringsmerken (zoals CE, FCC) en technische specificaties toont. Wordt geleverd met een zwarte stroomkabel, netjes vastgezet met een bindriem, die eindigt in een kleine cilindrische connector. Ideaal voor het voeden van laptops, het vervangen van verloren of beschadigde adapters en voor gebruikers die een betrouwbare stroomoplossing nodig hebben voor hun computerapparatuur."
+ },
+ "location": "Heerlen",
+ "quantity": 25,
+ "imageUrl": "/images/items/item-1762345213978-310163775.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T12:20:13.980Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T12:20:13.980Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b415835debc2b507e72cb"
+ },
+ "name": {
+ "en": "Power Cables.",
+ "nl": "Stroomkabels."
+ },
+ "description": {
+ "en": "Power Cables. Set of two black power cables featuring distinct plugs: one with a three-prong design and another labeled \"NRG Y\" with a three-hole structure. Both cables are constructed with a braided texture for enhanced durability. Ideal for connecting electronic devices to power outlets, replacing old or worn-out cables, and users needing reliable power cords for their equipment.",
+ "nl": "Stroomkabels. Set van twee zwarte stroomkabels met verschillende stekkers: een met een drijfstekker-ontwerp en een andere met het label \"NRG Y\" en een driegatenstructuur. Beide kabels zijn uitgevoerd met een geweven textuur voor extra duurzaamheid. Ideaal voor het aansluiten van elektronische apparaten op stopcontacten, het vervangen van oude of versleten kabels en voor gebruikers die betrouwbare stroomkabels nodig hebben voor hun apparatuur."
+ },
+ "location": "Sittard",
+ "quantity": 100,
+ "imageUrl": "/images/items/item-1762345304470-724719329.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T12:21:44.473Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T12:21:44.473Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b422b35debc2b507e72dc"
+ },
+ "name": {
+ "en": "Cisco Wireless Access Point.",
+ "nl": "Cisco Wireless Access Point."
+ },
+ "description": {
+ "en": "Cisco Wireless Access Point. White, oval-shaped network device with light gray edges, featuring a clean and modern design. The surface is printed with the \"Cisco\" logo and a signal wave icon. Ideal for expanding wireless network coverage in homes or offices, providing reliable Wi-Fi connectivity, and users seeking a high-quality, branded networking solution.",
+ "nl": "Cisco Wireless Access Point. Wit, ovaalvormig netwerkapparaat met lichtgrijze randen, met een strak en modern ontwerp. Het oppervlak is bedrukt met het \"Cisco\"-logo en een golfvormig icoon. Ideaal voor het uitbreiden van het draadloze netwerkbereik in huizen of kantoren, het bieden van betrouwbare Wi-Fi-connectiviteit en voor gebruikers die een hoogwaardige, merkgebonden netwerkoplossing zoeken."
+ },
+ "location": "Maastricht",
+ "quantity": 15,
+ "imageUrl": "/images/items/item-1762345515154-496202533.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T12:25:15.158Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T12:25:15.158Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b42aa35debc2b507e72e1"
+ },
+ "name": {
+ "en": "Spire Computer Cooling Fan.",
+ "nl": "Spire Computerkoeler."
+ },
+ "description": {
+ "en": "Spire Computer Cooling Fan. Black square cooling fan featuring a multi-blade design for efficient airflow. The center has a red and black label with the \"Spire\" brand name, \"CE\" and \"FC\" certifications, \"12V\" voltage, and the company website. The base includes mounting holes for easy installation. Ideal for improving computer cooling, enhancing system performance and longevity, and PC builders looking for a reliable and stylish cooling component.",
+ "nl": "Spire Computerkoeler. Zwarte, vierkante koelfan met een ontwerp met meerdere bladen voor een efficiënte luchtstroom. In het midden zit een rood-zwart label met de merknaam \"Spire\", \"CE\"- en \"FC\"-certificeringen, \"12V\"-spanning en de website van het bedrijf. De basis bevat montagegaten voor eenvoudige installatie. Ideaal voor het verbeteren van de computerkoeling, het verhogen van de systeemprestaties en levensduur, en voor PC-bouwers die op zoek zijn naar een betrouwbare en stijlvolle koelcomponent."
+ },
+ "location": "Sittard",
+ "quantity": 100,
+ "imageUrl": "/images/items/item-1762345642788-930429120.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T12:27:22.790Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T12:27:22.790Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b42e735debc2b507e72e8"
+ },
+ "name": {
+ "en": "Dell 65W Power Adapter.",
+ "nl": "Dell 65W Voeding."
+ },
+ "description": {
+ "en": "Dell 65W Power Adapter. Black power adapter with a labeled surface displaying \"65W\", \"Made in China\", model numbers \"CN - 06TM1C - 72438\" and \"57H - 80B3 - A04\", part number \"DP/N 06TM1C\", and various certification marks. Includes a power cable secured with a strap. Ideal for powering Dell laptops, replacing a lost or faulty adapter, and users requiring a genuine 65W power solution for their device.",
+ "nl": "Dell 65W Voeding. Zwarte voeding met een gelabeld oppervlak dat \"65W\", \"Made in China\", modelnummers \"CN - 06TM1C - 72438\" en \"57H - 80B3 - A04\", artikelnummer \"DP/N 06TM1C\" en diverse certificeringsmerken toont. Wordt geleverd met een stroomkabel die met een bindriem is vastgezet. Ideaal voor het voeden van Dell-laptops, het vervangen van een verloren of defecte adapter en voor gebruikers die een originele 65W-stroomoplossing voor hun apparaat nodig hebben."
+ },
+ "location": "Heerlen",
+ "quantity": 50,
+ "imageUrl": "/images/items/item-1762345703072-894447262.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T12:28:23.076Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T12:28:23.076Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b433935debc2b507e72ed"
+ },
+ "name": {
+ "en": "Electronic Device Interfaces.",
+ "nl": "Interfaces van Elektronische Apparaten."
+ },
+ "description": {
+ "en": "Electronic Device Interfaces. Close-up view of two multi-pin digital signal interfaces, such as HDMI. The left interface is equipped with a metal protective component, while the right one has no additional protection. The image clearly shows the metal construction and pin layout. Ideal for connecting high-definition audio/video devices, replacing damaged ports, and for technicians or users needing to identify specific digital connection types.",
+ "nl": "Interfaces van Elektronische Apparaten. Close-up van twee digitale signaalinterfaces met meerdere pinnen, zoals HDMI. De linkere interface is uitgerust met een metalen beschermingsonderdeel, terwijl de rechter geen extra bescherming heeft. De afbeelding toont duidelijk de metalen constructie en de indeling van de pinnen. Ideaal voor het aansluiten van high-definition audio-/videoapparaten, het vervangen van beschadigde poorten en voor technici of gebruikers die specifieke digitale connectietypes moeten identificeren."
+ },
+ "location": "Maastricht",
+ "quantity": 10,
+ "imageUrl": "/images/items/item-1762345785595-88237180.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T12:29:45.606Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T12:29:45.606Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b438735debc2b507e72f2"
+ },
+ "name": {
+ "en": "USB Data Cables.",
+ "nl": "USB-Datakabels."
+ },
+ "description": {
+ "en": "USB Data Cables. A collection of black USB data cables featuring various connectors, including USB-A to USB-B. Some cables are coiled and secured with straps for storage, while others are extended to showcase the connector details. The connectors bear the USB logo, and some cables are printed with text like \"CABLE\" and \"SPEED\". Ideal for connecting printers, external hard drives, and other peripherals, replacing old cables, and for users needing a versatile set of USB cables for their devices.",
+ "nl": "USB-Datakabels. Een verzameling zwarte USB-datakabels met diverse connectoren, waaronder USB-A naar USB-B. Sommige kabels zijn opgerold en met bindriemen vastgezet voor opslag, terwijl andere zijn uitgerekt om de connectordetails te tonen. De connectoren dragen het USB-logo en sommige kabels zijn bedrukt met tekst zoals \"CABLE\" en \"SPEED\". Ideaal voor het aansluiten van printers, externe harde schijven en andere randapparatuur, het vervangen van oude kabels en voor gebruikers die een veelzijdige set USB-kabels nodig hebben voor hun apparaten."
+ },
+ "location": "Maastricht",
+ "quantity": 10,
+ "imageUrl": "/images/items/item-1762345863362-70103036.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T12:31:03.364Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T12:31:03.364Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b43dd35debc2b507e72fb"
+ },
+ "name": {
+ "en": "Memory Modules.",
+ "nl": "Geheugenmodules."
+ },
+ "description": {
+ "en": "Memory Modules. Two memory modules featuring green circuit boards populated with multiple black rectangular memory chips. The bottom edge of each module has gold contact fingers (the \"connector\") for installation in a motherboard. Ideal for upgrading computer RAM, increasing system performance for multitasking and demanding applications, and for PC builders or technicians needing replacement or additional memory.",
+ "nl": "Geheugenmodules. Twee geheugenmodules met groene printplaten die zijn bevolkt met meerdere zwarte rechthoekige geheugenchips. De onderrand van elke module heeft gouden contactpinnen (de \"connector\") voor installatie in een moederbord. Ideaal voor het upgraden van computer-RAM, het verhogen van de systeemprestaties voor multitasking en veeleisende applicaties, en voor PC-bouwers of technici die vervangend of extra geheugen nodig hebben."
+ },
+ "location": "Maastricht",
+ "quantity": 100,
+ "imageUrl": "/images/items/item-1762345949231-699677075.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T12:32:29.233Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T12:32:29.233Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b444235debc2b507e7300"
+ },
+ "name": {
+ "en": "Fiber Optic Patch Cables.",
+ "nl": "Vezelkabels (Patchkabels)."
+ },
+ "description": {
+ "en": "Fiber Optic Patch Cables. Two yellow fiber optic patch cables, each terminated with blue LC-type connectors that include white protective caps. The cables feature an orange transition section and a beige protective boot near the connector. Ideal for high-speed network connections, linking network switches and routers in data centers, and for users requiring reliable, long-distance data transmission with minimal signal loss.",
+ "nl": "Vezelkabels (Patchkabels). Twee gele vezelkabels (patchkabels), elk afgesloten met blauwe LC-type connectoren die zijn voorzien van witte beschermkapjes. De kabels hebben een oranje overgangssectie en een beige beschermhoes nabij de connector. Ideaal voor hoogwaardige netwerkaansluitingen, het verbinden van netwerkswitches en routers in datacenters, en voor gebruikers die betrouwbare transmissie over lange afstanden nodig hebben met minimaal signaalverlies."
+ },
+ "location": "Sittard",
+ "quantity": 50,
+ "imageUrl": "/images/items/item-1762346050013-927798070.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T12:34:10.015Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T12:34:10.015Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b454735debc2b507e7313"
+ },
+ "name": {
+ "en": "Western Digital 160GB Hard Drive.",
+ "nl": "Western Digital 160GB Harde Schijf."
+ },
+ "description": {
+ "en": "Western Digital WD1600AAJS 160GB Hard Drive. Reliable 3.5-inch mechanical hard drive from the WD Caviar Blue series, offering 160GB of storage capacity. Features SATA interface and 8MB cache for efficient data transfer. Ideal for desktop computers, data storage, and system upgrades for users seeking dependable performance. Manufactured in Malaysia in December 2011.",
+ "nl": "Western Digital WD1600AAJS 160GB Harde Schijf. Betrouwbare 3,5-inch mechanische harde schijf uit de WD Caviar Blue-serie, met een opslagcapaciteit van 160GB. Uitgerust met SATA-interface en 8MB cache voor efficiënte datatransfer. Ideaal voor desktopcomputers, gegevensopslag en systeemupgrades voor gebruikers die op zoek zijn naar betrouwbare prestaties. Geproduceerd in Maleisië in december 2011."
+ },
+ "location": "Maastricht",
+ "quantity": 50,
+ "imageUrl": "/images/items/item-1762346311164-977510887.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T12:38:31.167Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T12:38:31.167Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b45b535debc2b507e7318"
+ },
+ "name": {
+ "en": "Western 500GB Hard Drive.",
+ "nl": "Western Digital 500GB Harde Schijf."
+ },
+ "description": {
+ "en": "Western Digital WD5000AZLX 500GB Hard Drive. High-performance 3.5-inch desktop hard drive from the WD Blue series, offering 500GB of storage capacity. Features SATA interface and 32MB cache for fast and efficient data transfer. Ideal for desktop PCs, external storage solutions, and system upgrades for users needing reliable performance and ample space. Designed with a sleek silver and black finish.",
+ "nl": "Western Digital WD5000AZLX 500GB Harde Schijf. Krachtige 3,5-inch desktop harde schijf uit de WD Blue-serie, met een opslagcapaciteit van 500GB. Uitgerust met SATA-interface en 32MB cache voor snelle en efficiënte datatransfer. Ideaal voor desktop-pc's, externe opslagoplossingen en systeemupgrades voor gebruikers die betrouwbare prestaties en voldoende ruimte nodig hebben. Ontworpen met een strakke zilveren en zwarte afwerking."
+ },
+ "location": "Maastricht",
+ "quantity": 50,
+ "imageUrl": "/images/items/item-1762346421502-666936247.jpeg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T12:40:21.504Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T12:40:21.504Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b45f235debc2b507e731d"
+ },
+ "name": {
+ "en": "Lenovo Laptop Power Adapter.",
+ "nl": "Lenovo Laptopvoeding."
+ },
+ "description": {
+ "en": "Lenovo Laptop Power Adapter. Original Lenovo power adapter designed for reliable and safe charging of Lenovo laptops. Features a compact black design with a fixed power cable, secured by a strap for easy storage and portability. The adapter body displays the Lenovo logo, various certification marks, and technical specifications. Ideal as a replacement or spare charger for home, office, or travel use.",
+ "nl": "Lenovo Laptopvoeding. Originele Lenovo-voeding ontworpen voor het betrouwbaar en veilig opladen van Lenovo-laptops. Heeft een compact zwart ontwerp met een vast netsnoer, dat met een riem is vastgezet voor eenvoudige opberging en draagbaarheid. Op de adapter staan het Lenovo-logo, diverse certificeringsmarkeringen en technische specificaties. Ideaal als vervangende of reserve lader voor thuis, op kantoor of voor onderweg."
+ },
+ "location": "Heerlen",
+ "quantity": 25,
+ "imageUrl": "/images/items/item-1762346482019-437770739.jpg",
+ "reserved": 0,
+ "createdAt": {
+ "$date": "2025-11-05T12:41:22.022Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T12:41:22.022Z"
+ },
+ "__v": 0
+}]
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/db/school_warehouse.reservations.json b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/db/school_warehouse.reservations.json
new file mode 100644
index 0000000..0637a08
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/db/school_warehouse.reservations.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/db/school_warehouse.users.json b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/db/school_warehouse.users.json
new file mode 100644
index 0000000..b83c910
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/db/school_warehouse.users.json
@@ -0,0 +1,47 @@
+[{
+ "_id": {
+ "$oid": "690b13c695d53f15a489ab86"
+ },
+ "username": "admin",
+ "password": "$2a$10$xUcDAWuSgK7tfz0uK5YahOf3MvuLGBivk/aR39PkkGsgLW/VkAoUC",
+ "role": "admin",
+ "createdAt": {
+ "$date": "2025-11-05T09:07:18.340Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T09:07:18.340Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b13c695d53f15a489ab88"
+ },
+ "username": "student",
+ "password": "$2a$10$JNYlFNqqw0FS03NQQidw/OQAgPkOvhbMlI2GvC2K6Ss8hhFkuvggy",
+ "email": "123456@vistacollege.nl",
+ "role": "student",
+ "createdAt": {
+ "$date": "2025-11-05T09:07:18.464Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T09:07:18.464Z"
+ },
+ "__v": 0
+},
+{
+ "_id": {
+ "$oid": "690b1b6959d6cb9b9a4695c4"
+ },
+ "username": "joey",
+ "password": "$2a$10$Netvip2ux/bJuERjWHbii.xV2drF3WYNLju1kbbkHQy256tk1PRQ6",
+ "email": "521924@vistacollege.nl",
+ "role": "student",
+ "createdAt": {
+ "$date": "2025-11-05T09:39:53.546Z"
+ },
+ "updatedAt": {
+ "$date": "2025-11-05T09:39:53.546Z"
+ },
+ "__v": 0
+}]
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/example-env b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/example-env
new file mode 100644
index 0000000..e1659cc
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/example-env
@@ -0,0 +1,3 @@
+PORT=your_webserver_port
+MONGODB_URI=mongodb://username:password@ip-or-domain:27017/your_database
+JWT_SECRET=your_jwt_secret_key_here
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/middleware/auth.js b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/middleware/auth.js
new file mode 100644
index 0000000..f8b996e
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/middleware/auth.js
@@ -0,0 +1,29 @@
+const jwt = require('jsonwebtoken');
+const User = require('../models/User');
+
+const auth = async (req, res, next) => {
+ try {
+ const token = req.header('Authorization').replace('Bearer ', '');
+ const decoded = jwt.verify(token, process.env.JWT_SECRET);
+ const user = await User.findOne({ _id: decoded.userId });
+
+ if (!user) {
+ throw new Error();
+ }
+
+ req.token = token;
+ req.user = user;
+ next();
+ } catch (error) {
+ res.status(401).send({ error: 'Please authenticate' });
+ }
+};
+
+const adminOnly = async (req, res, next) => {
+ if (req.user.role !== 'admin') {
+ return res.status(403).send({ error: 'Access denied' });
+ }
+ next();
+};
+
+module.exports = { auth, adminOnly };
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/models/Item.js b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/models/Item.js
new file mode 100644
index 0000000..6e295da
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/models/Item.js
@@ -0,0 +1,45 @@
+const mongoose = require('mongoose');
+
+const itemSchema = new mongoose.Schema({
+ name: {
+ en: {
+ type: String,
+ required: true
+ },
+ nl: {
+ type: String,
+ required: true
+ }
+ },
+ description: {
+ en: {
+ type: String,
+ default: ''
+ },
+ nl: {
+ type: String,
+ default: ''
+ }
+ },
+ location: {
+ type: String,
+ enum: ['Heerlen', 'Maastricht', 'Sittard'],
+ required: true
+ },
+ quantity: {
+ type: Number,
+ required: true,
+ min: 0
+ },
+ imageUrl: {
+ type: String,
+ default: '/images/default-item.png'
+ },
+ reserved: {
+ type: Number,
+ default: 0,
+ min: 0
+ }
+}, { timestamps: true });
+
+module.exports = mongoose.model('Item', itemSchema);
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/models/Reservation.js b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/models/Reservation.js
new file mode 100644
index 0000000..b93d56f
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/models/Reservation.js
@@ -0,0 +1,31 @@
+const mongoose = require('mongoose');
+
+const reservationSchema = new mongoose.Schema({
+ itemId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'Item',
+ required: true
+ },
+ quantity: {
+ type: Number,
+ required: true,
+ min: 1,
+ default: 1
+ },
+ userId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'User',
+ required: true
+ },
+ status: {
+ type: String,
+ enum: ['PENDING', 'APPROVED', 'REJECTED', 'RETURN_PENDING', 'RETURNED', 'ARCHIVED'],
+ default: 'PENDING'
+ },
+ reservedDate: {
+ type: Date,
+ default: Date.now
+ }
+}, { timestamps: true });
+
+module.exports = mongoose.model('Reservation', reservationSchema);
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/models/User.js b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/models/User.js
new file mode 100644
index 0000000..cac7326
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/models/User.js
@@ -0,0 +1,34 @@
+const mongoose = require('mongoose');
+
+const userSchema = new mongoose.Schema({
+ username: {
+ type: String,
+ required: true,
+ unique: true
+ },
+ password: {
+ type: String,
+ required: true
+ },
+ email: {
+ type: String,
+ required: function() {
+ return this.role === 'student';
+ },
+ sparse: true,
+ unique: true,
+ validate: {
+ validator: function(email) {
+ return /^\d{6}@vistacollege\.nl$/.test(email);
+ },
+ message: 'Email must be in the format: 123456@vistacollege.nl'
+ }
+ },
+ role: {
+ type: String,
+ enum: ['admin', 'student'],
+ required: true
+ }
+}, { timestamps: true });
+
+module.exports = mongoose.model('User', userSchema);
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/package-lock.json b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/package-lock.json
new file mode 100644
index 0000000..aab4f26
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/package-lock.json
@@ -0,0 +1,1769 @@
+{
+ "name": "school-warehouse-system",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "school-warehouse-system",
+ "version": "1.0.0",
+ "dependencies": {
+ "bcryptjs": "^2.4.3",
+ "cors": "^2.8.5",
+ "dotenv": "^16.3.1",
+ "express": "^4.18.2",
+ "jsonwebtoken": "^9.0.2",
+ "mongoose": "^7.6.3",
+ "multer": "^2.0.2"
+ },
+ "devDependencies": {
+ "nodemon": "^3.0.1"
+ }
+ },
+ "node_modules/@mongodb-js/saslprep": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.1.tgz",
+ "integrity": "sha512-6nZrq5kfAz0POWyhljnbWQQJQ5uT8oE2ddX303q1uY0tWsivWKgBDXBBvuFPwOqRRalXJuVO9EjOdVtuhLX0zg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "sparse-bitfield": "^3.0.3"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "24.7.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz",
+ "integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.14.0"
+ }
+ },
+ "node_modules/@types/webidl-conversions": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
+ "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/whatwg-url": {
+ "version": "8.2.2",
+ "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz",
+ "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/webidl-conversions": "*"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/append-field": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
+ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
+ "license": "MIT"
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/bcryptjs": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
+ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
+ "license": "MIT"
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.13.0",
+ "raw-body": "2.5.2",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/bson": {
+ "version": "5.5.1",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz",
+ "integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14.20.1"
+ }
+ },
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "license": "MIT"
+ },
+ "node_modules/busboy": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+ "dependencies": {
+ "streamsearch": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=10.16.0"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
+ "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
+ "engines": [
+ "node >= 6.0"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.0.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.3",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.7.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.3.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.13.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.19.0",
+ "serve-static": "1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ip-address": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
+ "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+ "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "jws": "^3.2.2",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jsonwebtoken/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/jwa": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
+ "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^1.4.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/kareem": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz",
+ "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+ "license": "MIT"
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/memory-pager": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
+ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
+ "node_modules/mongodb": {
+ "version": "5.9.2",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz",
+ "integrity": "sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bson": "^5.5.0",
+ "mongodb-connection-string-url": "^2.6.0",
+ "socks": "^2.7.1"
+ },
+ "engines": {
+ "node": ">=14.20.1"
+ },
+ "optionalDependencies": {
+ "@mongodb-js/saslprep": "^1.1.0"
+ },
+ "peerDependencies": {
+ "@aws-sdk/credential-providers": "^3.188.0",
+ "@mongodb-js/zstd": "^1.0.0",
+ "kerberos": "^1.0.0 || ^2.0.0",
+ "mongodb-client-encryption": ">=2.3.0 <3",
+ "snappy": "^7.2.2"
+ },
+ "peerDependenciesMeta": {
+ "@aws-sdk/credential-providers": {
+ "optional": true
+ },
+ "@mongodb-js/zstd": {
+ "optional": true
+ },
+ "kerberos": {
+ "optional": true
+ },
+ "mongodb-client-encryption": {
+ "optional": true
+ },
+ "snappy": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/mongodb-connection-string-url": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz",
+ "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/whatwg-url": "^8.2.1",
+ "whatwg-url": "^11.0.0"
+ }
+ },
+ "node_modules/mongoose": {
+ "version": "7.8.7",
+ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.8.7.tgz",
+ "integrity": "sha512-5Bo4CrUxrPITrhMKsqUTOkXXo2CoRC5tXxVQhnddCzqDMwRXfyStrxj1oY865g8gaekSBhxAeNkYyUSJvGm9Hw==",
+ "license": "MIT",
+ "dependencies": {
+ "bson": "^5.5.0",
+ "kareem": "2.5.1",
+ "mongodb": "5.9.2",
+ "mpath": "0.9.0",
+ "mquery": "5.0.0",
+ "ms": "2.1.3",
+ "sift": "16.0.1"
+ },
+ "engines": {
+ "node": ">=14.20.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mongoose"
+ }
+ },
+ "node_modules/mongoose/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/mpath": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
+ "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/mquery": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz",
+ "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4.x"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/mquery/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/mquery/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/multer": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz",
+ "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==",
+ "license": "MIT",
+ "dependencies": {
+ "append-field": "^1.0.0",
+ "busboy": "^1.6.0",
+ "concat-stream": "^2.0.0",
+ "mkdirp": "^0.5.6",
+ "object-assign": "^4.1.1",
+ "type-is": "^1.6.18",
+ "xtend": "^4.0.2"
+ },
+ "engines": {
+ "node": ">= 10.16.0"
+ }
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/nodemon": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
+ "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^3.1.2",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/nodemon/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/nodemon/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "license": "MIT"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.19.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/sift": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz",
+ "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==",
+ "license": "MIT"
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/smart-buffer": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks": {
+ "version": "2.8.7",
+ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
+ "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
+ "license": "MIT",
+ "dependencies": {
+ "ip-address": "^10.0.1",
+ "smart-buffer": "^4.2.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/sparse-bitfield": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
+ "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "memory-pager": "^1.0.2"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/streamsearch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
+ "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
+ "license": "MIT"
+ },
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/undici-types": {
+ "version": "7.14.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
+ "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
+ "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^3.0.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ }
+ }
+}
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/add-item.html b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/add-item.html
new file mode 100644
index 0000000..56dd2c0
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/add-item.html
@@ -0,0 +1,121 @@
+
+
+
+
+
+ Add New Item - Admin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/admin-reservations.html b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/admin-reservations.html
new file mode 100644
index 0000000..c1cd044
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/admin-reservations.html
@@ -0,0 +1,106 @@
+
+
+
+
+
+ Manage Reservations - Admin
+
+
+
+
+
+
+
+
+
+
Manage Reservations
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Student |
+ Item Name |
+ Quantity |
+ Location |
+ Reserved Date |
+ Status |
+ Actions |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/admin.html b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/admin.html
new file mode 100644
index 0000000..60d158e
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/admin.html
@@ -0,0 +1,177 @@
+
+
+
+
+
+ Magazijn Dashboard - Admin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Image |
+ Item Name |
+ Description |
+ Location |
+ Quantity |
+ Reserved |
+ Actions |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/css/style.css b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/css/style.css
new file mode 100644
index 0000000..e3dc0dc
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/css/style.css
@@ -0,0 +1,589 @@
+/* Vista College Colors */
+:root {
+ --vista-white: #FEFEFE;
+ --vista-grey: #CCCECF;
+ --vista-coral: #D3705A;
+ --vista-peach: #CD977E;
+ --vista-blue: #1F4952;
+}
+
+/* Custom styles */
+.navbar {
+ margin-bottom: 2rem;
+ background-color: var(--vista-blue) !important;
+}
+
+.navbar-dark .navbar-nav .nav-link {
+ color: var(--vista-white);
+}
+
+.navbar-dark .navbar-nav .nav-link:hover {
+ color: var(--vista-grey);
+}
+
+.card {
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ height: 100%;
+ background-color: var(--vista-white);
+ border: none;
+}
+
+
+.table {
+ background-color: var(--vista-white);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+}
+
+/* Grid view styles */
+.item-card {
+ transition: transform 0.2s;
+}
+
+.item-card:hover {
+ transform: translateY(-5px);
+}
+
+.item-image {
+ width: 100%;
+ height: 200px;
+ object-fit: cover;
+ border-top-left-radius: calc(0.375rem - 1px);
+ border-top-right-radius: calc(0.375rem - 1px);
+}
+
+.item-card .card-body {
+ display: flex;
+ flex-direction: column;
+}
+
+.item-card .card-footer {
+ background-color: transparent;
+ border-top: none;
+ padding-top: 0;
+}
+
+/* Image preview */
+#imagePreview img {
+ max-width: 200px;
+ max-height: 200px;
+ object-fit: contain;
+ border-radius: 4px;
+}
+
+/* List view image thumbnail */
+.item-thumbnail {
+ width: 50px;
+ height: 50px;
+ object-fit: cover;
+ border-radius: 4px;
+}
+
+.btn {
+ margin: 2px;
+}
+
+.btn-primary {
+ background-color: var(--vista-coral);
+ border-color: var(--vista-coral);
+}
+
+.btn-primary:hover {
+ background-color: var(--vista-peach);
+ border-color: var(--vista-peach);
+}
+
+.btn-outline-primary {
+ color: var(--vista-coral);
+ border-color: var(--vista-coral);
+}
+
+.btn-outline-primary:hover {
+ background-color: var(--vista-coral);
+ border-color: var(--vista-coral);
+}
+
+.btn-outline-primary.active {
+ background-color: var(--vista-coral) !important;
+ border-color: var(--vista-coral) !important;
+}
+
+.form-control:focus, .form-select:focus {
+ border-color: var(--vista-coral);
+ box-shadow: 0 0 0 0.25rem rgba(211, 112, 90, 0.25);
+}
+
+body {
+ background-color: var(--vista-grey);
+}
+
+.navbar-brand {
+ font-weight: bold;
+ color: var(--vista-white) !important;
+}
+
+.card-header {
+ background-color: var(--vista-blue);
+ border-bottom: none;
+ color: var(--vista-white);
+}
+
+.table-responsive {
+ border-radius: 0.25rem;
+}
+
+.table-striped tbody tr:nth-of-type(odd) {
+ background-color: rgba(204, 206, 207, 0.1);
+}
+
+.badge.bg-success {
+ background-color: var(--vista-coral) !important;
+}
+
+.badge.bg-secondary {
+ background-color: var(--vista-grey) !important;
+}
+
+/* Modal styling */
+.modal-header {
+ background-color: var(--vista-blue);
+ color: var(--vista-white);
+}
+
+.modal-header .btn-close {
+ color: var(--vista-white);
+ opacity: 1;
+}
+
+/* Modal image styling */
+.modal-image-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 300px;
+ margin-bottom: 1rem;
+ background-color: var(--vista-grey);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.modal-item-image {
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: contain;
+}
+
+/* Status badges */
+.badge {
+ padding: 0.5em 1em;
+}
+
+.reservation-pending {
+ background-color: #ffc107;
+ color: #000;
+}
+
+.reservation-approved {
+ background-color: var(--vista-coral);
+ color: var(--vista-white);
+}
+
+.reservation-rejected {
+ background-color: var(--vista-grey);
+ color: var(--vista-blue);
+}
+
+/* Table styling */
+.table-dark {
+ background-color: var(--vista-blue) !important;
+ color: var(--vista-white) !important;
+}
+
+.table-dark th {
+ border-color: rgba(255, 255, 255, 0.1);
+ font-weight: 600;
+ padding: 12px 8px;
+ font-size: 0.9rem;
+ background-color: var(--vista-blue) !important;
+ color: var(--vista-white) !important;
+}
+
+.table tbody tr:hover {
+ background-color: rgba(211, 112, 90, 0.1) !important;
+}
+
+.table td {
+ vertical-align: middle;
+ padding: 12px 8px;
+ font-size: 0.9rem;
+ border-color: var(--vista-grey) !important;
+}
+
+.table-striped tbody tr:nth-of-type(odd) {
+ background-color: rgba(204, 206, 207, 0.05) !important;
+}
+
+.table-striped tbody tr:nth-of-type(even) {
+ background-color: var(--vista-white) !important;
+}
+
+/* Specific column widths for reservations table */
+.reservations-table th:nth-child(1), /* Student */
+.reservations-table td:nth-child(1) {
+ width: 12%;
+ min-width: 90px;
+}
+
+.reservations-table th:nth-child(2), /* Item Name */
+.reservations-table td:nth-child(2) {
+ width: 22%;
+ min-width: 130px;
+}
+
+.reservations-table th:nth-child(3), /* Quantity */
+.reservations-table td:nth-child(3) {
+ width: 8%;
+ min-width: 60px;
+ text-align: center;
+}
+
+.reservations-table th:nth-child(4), /* Location */
+.reservations-table td:nth-child(4) {
+ width: 12%;
+ min-width: 90px;
+ text-align: center;
+}
+
+.reservations-table th:nth-child(5), /* Reserved Date */
+.reservations-table td:nth-child(5) {
+ width: 13%;
+ min-width: 100px;
+ text-align: center;
+}
+
+.reservations-table th:nth-child(6), /* Status */
+.reservations-table td:nth-child(6) {
+ width: 10%;
+ min-width: 80px;
+ text-align: center;
+}
+
+.reservations-table th:nth-child(7), /* Actions */
+.reservations-table td:nth-child(7) {
+ width: 23%;
+ min-width: 140px;
+ text-align: center;
+}
+
+/* Badge styling for better visibility */
+.badge {
+ font-size: 0.75rem;
+ padding: 0.4em 0.8em;
+ font-weight: 600;
+}
+
+/* Vista-colored status badges */
+.reservation-pending {
+ background-color: #ffc107 !important;
+ color: var(--vista-blue) !important;
+ border: 1px solid var(--vista-peach);
+}
+
+.reservation-approved {
+ background-color: var(--vista-coral) !important;
+ color: var(--vista-white) !important;
+ border: 1px solid var(--vista-coral);
+}
+
+.reservation-rejected {
+ background-color: var(--vista-grey) !important;
+ color: var(--vista-blue) !important;
+ border: 1px solid var(--vista-grey);
+}
+
+.reservation-return_pending {
+ background-color: #ffc107 !important;
+ color: var(--vista-blue) !important;
+ border: 1px solid var(--vista-peach);
+}
+
+.reservation-returned {
+ background-color: var(--vista-peach) !important;
+ color: var(--vista-white) !important;
+ border: 1px solid var(--vista-peach);
+}
+
+.reservation-archived {
+ background-color: #6c757d !important;
+ color: var(--vista-white) !important;
+ border: 1px solid #6c757d;
+}
+
+/* Specific column widths for student reservations table */
+.student-reservations-table th:nth-child(1), /* Item Name */
+.student-reservations-table td:nth-child(1) {
+ width: 25%;
+ min-width: 150px;
+}
+
+.student-reservations-table th:nth-child(2), /* Quantity */
+.student-reservations-table td:nth-child(2) {
+ width: 10%;
+ min-width: 70px;
+ text-align: center;
+}
+
+.student-reservations-table th:nth-child(3), /* Location */
+.student-reservations-table td:nth-child(3) {
+ width: 15%;
+ min-width: 100px;
+ text-align: center;
+}
+
+.student-reservations-table th:nth-child(4), /* Reserved Date */
+.student-reservations-table td:nth-child(4) {
+ width: 15%;
+ min-width: 110px;
+ text-align: center;
+}
+
+.student-reservations-table th:nth-child(5), /* Status */
+.student-reservations-table td:nth-child(5) {
+ width: 15%;
+ min-width: 100px;
+ text-align: center;
+}
+
+.student-reservations-table th:nth-child(6), /* Actions */
+.student-reservations-table td:nth-child(6) {
+ width: 20%;
+ min-width: 120px;
+ text-align: center;
+}
+
+/* Quantity badge styling */
+.badge.bg-info {
+ background-color: var(--vista-blue) !important;
+ color: var(--vista-white) !important;
+ font-weight: 700;
+ font-size: 0.8rem;
+}
+
+/* Button group styling */
+.btn-group-sm .btn {
+ font-size: 0.7rem;
+ padding: 0.2rem 0.4rem;
+ border-radius: 0.2rem;
+}
+
+.btn-group .btn {
+ white-space: nowrap;
+ margin: 0 1px;
+}
+
+.btn-group .btn i {
+ font-size: 0.8rem;
+}
+
+/* Responsive button text */
+@media (max-width: 768px) {
+ .btn-group .btn {
+ font-size: 0.65rem;
+ padding: 0.15rem 0.3rem;
+ }
+
+ .btn-group .btn .btn-text {
+ display: none;
+ }
+}
+
+/* Action buttons with Vista colors */
+.btn-success {
+ background-color: var(--vista-coral) !important;
+ border-color: var(--vista-coral) !important;
+ color: var(--vista-white) !important;
+}
+
+.btn-success:hover {
+ background-color: var(--vista-peach) !important;
+ border-color: var(--vista-peach) !important;
+ color: var(--vista-white) !important;
+}
+
+.btn-danger {
+ background-color: var(--vista-grey) !important;
+ border-color: var(--vista-grey) !important;
+ color: var(--vista-blue) !important;
+}
+
+.btn-danger:hover {
+ background-color: #999 !important;
+ border-color: #999 !important;
+ color: var(--vista-white) !important;
+}
+
+.btn-warning {
+ background-color: var(--vista-peach) !important;
+ border-color: var(--vista-peach) !important;
+ color: var(--vista-white) !important;
+}
+
+.btn-warning:hover {
+ background-color: var(--vista-coral) !important;
+ border-color: var(--vista-coral) !important;
+ color: var(--vista-white) !important;
+}
+
+.btn-info {
+ background-color: var(--vista-blue) !important;
+ border-color: var(--vista-blue) !important;
+ color: var(--vista-white) !important;
+}
+
+.btn-info:hover {
+ background-color: var(--vista-peach) !important;
+ border-color: var(--vista-peach) !important;
+ color: var(--vista-white) !important;
+}
+
+/* Small button spacing */
+.btn-sm {
+ margin: 0 2px;
+}
+
+/* Simple Search Bar Styling */
+.search-input-group .input-group-text {
+ background-color: var(--vista-coral);
+ border-color: var(--vista-coral);
+ color: var(--vista-white);
+}
+
+.search-input-group .form-control {
+ border-color: var(--vista-coral);
+}
+
+.search-input-group .form-control:focus {
+ border-color: var(--vista-coral);
+ box-shadow: 0 0 0 0.25rem rgba(211, 112, 90, 0.25);
+}
+
+.search-input-group .btn-outline-secondary {
+ border-color: var(--vista-coral);
+ color: var(--vista-coral);
+}
+
+.search-input-group .btn-outline-secondary:hover {
+ background-color: var(--vista-coral);
+ border-color: var(--vista-coral);
+ color: var(--vista-white);
+}
+
+/* Language Toggle Styles - Modern Button Design */
+.language-toggle {
+ display: flex;
+ align-items: center;
+ margin-right: 1rem;
+}
+
+.language-switcher {
+ display: flex;
+ background-color: rgba(255, 255, 255, 0.1);
+ border-radius: 25px;
+ padding: 2px;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ backdrop-filter: blur(10px);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ transition: all 0.3s ease;
+}
+
+.language-switcher:hover {
+ background-color: rgba(255, 255, 255, 0.15);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ transform: translateY(-1px);
+}
+
+.language-btn {
+ background: none;
+ border: none;
+ padding: 8px 16px;
+ margin: 0;
+ border-radius: 20px;
+ color: rgba(255, 255, 255, 0.7);
+ font-weight: 500;
+ font-size: 0.9rem;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ position: relative;
+ overflow: hidden;
+}
+
+.language-btn:hover {
+ color: rgba(255, 255, 255, 0.9);
+ transform: scale(1.05);
+}
+
+.language-btn.active {
+ background: linear-gradient(135deg, var(--vista-blue), var(--vista-coral));
+ color: white;
+ font-weight: 600;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
+}
+
+.language-btn.active::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.1) 50%, transparent 70%);
+ animation: shine 2s ease-in-out infinite;
+}
+
+@keyframes shine {
+ 0% { transform: translateX(-100%); }
+ 50% { transform: translateX(100%); }
+ 100% { transform: translateX(100%); }
+}
+
+/* Focus styles for accessibility */
+.language-btn:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5);
+}
+
+.language-btn:active {
+ transform: scale(0.98);
+}
+
+/* Language toggle for login page */
+.language-toggle-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.language-toggle-container .language-toggle {
+ margin-right: 0;
+}
+
+.language-switcher.login-page {
+ background-color: rgba(21, 78, 135, 0.1);
+ border: 1px solid rgba(21, 78, 135, 0.2);
+}
+
+.language-switcher.login-page:hover {
+ background-color: rgba(21, 78, 135, 0.15);
+}
+
+.language-switcher.login-page .language-btn {
+ color: var(--vista-blue);
+}
+
+.language-switcher.login-page .language-btn:hover {
+ color: var(--vista-coral);
+}
+
+.language-switcher.login-page .language-btn.active {
+ background: linear-gradient(135deg, var(--vista-blue), var(--vista-coral));
+ color: white;
+}
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/favicon.svg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/favicon.svg
new file mode 100644
index 0000000..898a8fb
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/favicon.svg
@@ -0,0 +1,16 @@
+
+
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/default-item.png b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/default-item.png
new file mode 100644
index 0000000..9eb137f
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/default-item.png differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762333952965-217359885.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762333952965-217359885.jpg
new file mode 100644
index 0000000..4f8eb37
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762333952965-217359885.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762336186624-692937132.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762336186624-692937132.jpg
new file mode 100644
index 0000000..8e42f4c
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762336186624-692937132.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762336324397-286054098.png b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762336324397-286054098.png
new file mode 100644
index 0000000..271fc48
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762336324397-286054098.png differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762336537233-258395909.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762336537233-258395909.jpg
new file mode 100644
index 0000000..8a2388c
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762336537233-258395909.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762336610278-294967324.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762336610278-294967324.jpg
new file mode 100644
index 0000000..4272bb8
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762336610278-294967324.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762336810580-276271022.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762336810580-276271022.jpg
new file mode 100644
index 0000000..ffdc40a
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762336810580-276271022.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762336867904-363624133.png b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762336867904-363624133.png
new file mode 100644
index 0000000..aa106a3
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762336867904-363624133.png differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762336958564-240058321.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762336958564-240058321.jpg
new file mode 100644
index 0000000..9921b5c
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762336958564-240058321.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337062917-846000877.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337062917-846000877.jpg
new file mode 100644
index 0000000..22153d8
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337062917-846000877.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337122183-65891618.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337122183-65891618.jpg
new file mode 100644
index 0000000..14e3f30
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337122183-65891618.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337231487-235622162.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337231487-235622162.jpg
new file mode 100644
index 0000000..1a945b7
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337231487-235622162.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337329644-263204031.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337329644-263204031.jpg
new file mode 100644
index 0000000..284d735
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337329644-263204031.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337407188-273253468.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337407188-273253468.jpg
new file mode 100644
index 0000000..aab091a
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337407188-273253468.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337485383-599696829.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337485383-599696829.jpg
new file mode 100644
index 0000000..5eff1dd
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337485383-599696829.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337573536-112460822.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337573536-112460822.jpg
new file mode 100644
index 0000000..2351edd
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337573536-112460822.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337647152-811870729.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337647152-811870729.jpg
new file mode 100644
index 0000000..38274e1
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762337647152-811870729.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762338530678-745200924.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762338530678-745200924.jpg
new file mode 100644
index 0000000..c9b6125
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762338530678-745200924.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762338596125-667343123.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762338596125-667343123.jpg
new file mode 100644
index 0000000..a1c524c
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762338596125-667343123.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762338700058-216183679.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762338700058-216183679.jpg
new file mode 100644
index 0000000..b140abd
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762338700058-216183679.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762338770859-626525192.png b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762338770859-626525192.png
new file mode 100644
index 0000000..1e12f11
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762338770859-626525192.png differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762339337231-860786354.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762339337231-860786354.jpg
new file mode 100644
index 0000000..ae7eea8
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762339337231-860786354.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762339393022-389531031.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762339393022-389531031.jpg
new file mode 100644
index 0000000..3982383
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762339393022-389531031.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762343487683-425439694.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762343487683-425439694.jpg
new file mode 100644
index 0000000..e252e18
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762343487683-425439694.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762343598522-590746235.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762343598522-590746235.jpg
new file mode 100644
index 0000000..ba10490
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762343598522-590746235.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762343690686-711501396.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762343690686-711501396.jpg
new file mode 100644
index 0000000..ad057e6
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762343690686-711501396.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345050512-670926874.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345050512-670926874.jpg
new file mode 100644
index 0000000..13ae316
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345050512-670926874.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345213978-310163775.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345213978-310163775.jpg
new file mode 100644
index 0000000..c584a79
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345213978-310163775.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345304470-724719329.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345304470-724719329.jpg
new file mode 100644
index 0000000..35d7459
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345304470-724719329.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345515154-496202533.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345515154-496202533.jpg
new file mode 100644
index 0000000..df2dcd5
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345515154-496202533.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345642788-930429120.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345642788-930429120.jpg
new file mode 100644
index 0000000..18ec073
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345642788-930429120.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345703072-894447262.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345703072-894447262.jpg
new file mode 100644
index 0000000..0e5825b
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345703072-894447262.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345785595-88237180.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345785595-88237180.jpg
new file mode 100644
index 0000000..27f2000
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345785595-88237180.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345863362-70103036.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345863362-70103036.jpg
new file mode 100644
index 0000000..3520c36
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345863362-70103036.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345949231-699677075.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345949231-699677075.jpg
new file mode 100644
index 0000000..fd54efc
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762345949231-699677075.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762346050013-927798070.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762346050013-927798070.jpg
new file mode 100644
index 0000000..aca2767
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762346050013-927798070.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762346311164-977510887.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762346311164-977510887.jpg
new file mode 100644
index 0000000..15e49a7
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762346311164-977510887.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762346421502-666936247.jpeg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762346421502-666936247.jpeg
new file mode 100644
index 0000000..23383e9
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762346421502-666936247.jpeg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762346482019-437770739.jpg b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762346482019-437770739.jpg
new file mode 100644
index 0000000..941963d
Binary files /dev/null and b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/images/items/item-1762346482019-437770739.jpg differ
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/index.html b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/index.html
new file mode 100644
index 0000000..a6e656c
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/index.html
@@ -0,0 +1,55 @@
+
+
+
+
+
+ School Magazijn Systeem
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/add-item.js b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/add-item.js
new file mode 100644
index 0000000..e534cc0
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/add-item.js
@@ -0,0 +1,135 @@
+// Add Item page functionality
+
+// Check authentication
+function checkAuth() {
+ const token = localStorage.getItem('token');
+ const role = localStorage.getItem('userRole');
+
+ if (!token || role !== 'admin') {
+ window.location.href = '/index.html';
+ }
+}
+
+// Initialize page
+function initializePage() {
+ checkAuth();
+ setupEventListeners();
+ displayUserInfo();
+ setupImagePreview();
+}
+
+// Display user info
+function displayUserInfo() {
+ const username = localStorage.getItem('username');
+ document.getElementById('userInfo').textContent = `Admin: ${username}`;
+}
+
+// Set up event listeners
+function setupEventListeners() {
+ document.getElementById('addItemForm').addEventListener('submit', addItem);
+ document.getElementById('logoutBtn').addEventListener('click', logout);
+}
+
+// Handle logout
+function logout() {
+ localStorage.removeItem('token');
+ localStorage.removeItem('userRole');
+ localStorage.removeItem('username');
+ window.location.href = '/index.html';
+}
+
+// Set up image preview
+function setupImagePreview() {
+ const imageInput = document.getElementById('itemImage');
+ const previewContainer = document.getElementById('imagePreview');
+ const removeButton = document.getElementById('removeImage');
+
+ imageInput.addEventListener('change', () => {
+ const file = imageInput.files[0];
+ if (file) {
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ previewContainer.innerHTML = `
`;
+ removeButton.style.display = 'block';
+ };
+ reader.readAsDataURL(file);
+ } else {
+ previewContainer.innerHTML = '';
+ removeButton.style.display = 'none';
+ }
+ });
+
+ // Handle remove button click
+ removeButton.addEventListener('click', () => {
+ imageInput.value = '';
+ previewContainer.innerHTML = '';
+ removeButton.style.display = 'none';
+ });
+}
+
+// Add new item
+async function addItem(e) {
+ e.preventDefault();
+
+ const formData = new FormData();
+
+ // Collect multilingual name data
+ const nameData = {
+ nl: document.getElementById('itemNameNl').value.trim(),
+ en: document.getElementById('itemNameEn').value.trim()
+ };
+
+ // Collect multilingual description data
+ const descriptionData = {
+ nl: document.getElementById('itemDescriptionNl').value.trim(),
+ en: document.getElementById('itemDescriptionEn').value.trim()
+ };
+
+ // Validate that at least one language is filled for name
+ if (!nameData.nl && !nameData.en) {
+ alert('Please provide at least one name (Dutch or English)');
+ return;
+ }
+
+ // If only one language is provided for name, copy it to the other
+ if (!nameData.nl && nameData.en) {
+ nameData.nl = nameData.en;
+ }
+ if (!nameData.en && nameData.nl) {
+ nameData.en = nameData.nl;
+ }
+
+ formData.append('name', JSON.stringify(nameData));
+ formData.append('location', document.getElementById('itemLocation').value);
+ formData.append('description', JSON.stringify(descriptionData));
+ formData.append('quantity', document.getElementById('itemQuantity').value);
+
+ const imageFile = document.getElementById('itemImage').files[0];
+ if (imageFile) {
+ formData.append('image', imageFile);
+ }
+
+ try {
+ const response = await fetch('/api/items', {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ },
+ body: formData
+ });
+
+ if (response.ok) {
+ alert('Item added successfully!');
+ window.location.href = 'admin.html';
+ } else {
+ const error = await response.json();
+ alert(error.message || 'Failed to add item');
+ }
+ } catch (error) {
+ console.error('Error adding item:', error);
+ alert('Failed to add item');
+ }
+}
+
+// Initialize the page when loaded
+document.addEventListener('DOMContentLoaded', initializePage);
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/admin-reservations.js b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/admin-reservations.js
new file mode 100644
index 0000000..393089a
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/admin-reservations.js
@@ -0,0 +1,281 @@
+// Admin reservations page functionality
+let reservations = [];
+
+// Check authentication
+function checkAuth() {
+ const token = localStorage.getItem('token');
+ const role = localStorage.getItem('userRole');
+
+ if (!token || role !== 'admin') {
+ window.location.href = '/index.html';
+ }
+}
+
+// Initialize page
+async function initializePage() {
+ checkAuth();
+ await loadReservations();
+ setupEventListeners();
+ displayUserInfo();
+ startAutoRefresh();
+}
+
+// Display user info
+function displayUserInfo() {
+ const username = localStorage.getItem('username');
+ document.getElementById('userInfo').textContent = `Admin: ${username}`;
+}
+
+// Load and filter reservations
+async function loadReservations() {
+ try {
+ const response = await fetch('/api/reservations', {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+ reservations = await response.json();
+ filterAndDisplayReservations();
+ } catch (error) {
+ console.error('Error loading reservations:', error);
+ alert('Failed to load reservations');
+ }
+}
+
+// Filter and display reservations
+function filterAndDisplayReservations() {
+ const locationFilter = document.getElementById('locationFilter').value;
+ const statusFilter = document.getElementById('statusFilter').value;
+ const searchTerm = document.getElementById('searchInput').value.toLowerCase();
+
+ const filteredReservations = reservations.filter(reservation => {
+ const locationMatch = locationFilter === 'all' || reservation.location === locationFilter;
+ const statusMatch = statusFilter === 'all' || reservation.status === statusFilter;
+ const searchMatch = searchTerm === '' ||
+ reservation.studentName.toLowerCase().includes(searchTerm) ||
+ reservation.itemName.toLowerCase().includes(searchTerm) ||
+ reservation.location.toLowerCase().includes(searchTerm);
+ return locationMatch && statusMatch && searchMatch;
+ });
+
+ const reservationsList = document.getElementById('reservationsList');
+ reservationsList.innerHTML = filteredReservations.map(reservation => `
+
+ | ${reservation.studentName} |
+ ${reservation.itemName} |
+ ${reservation.quantity || 1} |
+ ${reservation.location} |
+ ${new Date(reservation.reservedDate).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric'
+ })} |
+ ${reservation.status} |
+
+
+ ${reservation.status === 'PENDING' ? `
+
+
+ ` : reservation.status === 'RETURN_PENDING' ? `
+
+
+ ` : reservation.status === 'APPROVED' ? `
+
+ ` : reservation.status === 'RETURNED' ? `
+
+ ` : ''}
+
+
+ |
+
+ `).join('');
+}
+
+// Update reservation status
+async function updateReservation(reservationId, status) {
+ try {
+ const response = await fetch(`/api/reservations/${reservationId}`, {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ },
+ body: JSON.stringify({ status })
+ });
+
+ if (response.ok) {
+ await loadReservations();
+ } else {
+ const error = await response.json();
+ alert(error.message || 'Failed to update reservation');
+ }
+ } catch (error) {
+ console.error('Error updating reservation:', error);
+ alert('Failed to update reservation');
+ }
+}
+
+// Delete reservation
+async function deleteReservation(reservationId) {
+ if (!confirm('Are you sure you want to delete this reservation?')) return;
+
+ try {
+ const response = await fetch(`/api/reservations/${reservationId}`, {
+ method: 'DELETE',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+
+ if (response.ok) {
+ await loadReservations();
+ } else {
+ const error = await response.json();
+ alert(error.message || 'Failed to delete reservation');
+ }
+ } catch (error) {
+ console.error('Error deleting reservation:', error);
+ alert('Failed to delete reservation');
+ }
+}
+
+// Archive reservation
+async function archiveReservation(reservationId) {
+ if (!confirm('Are you sure you want to archive this reservation? It will be hidden from the list but remain in the database.')) return;
+
+ try {
+ const response = await fetch(`/api/reservations/${reservationId}/archive`, {
+ method: 'PATCH',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+
+ if (response.ok) {
+ await loadReservations();
+ // Show success message
+ const alert = document.createElement('div');
+ alert.className = 'alert alert-success alert-dismissible fade show';
+ alert.innerHTML = `
+ Reservation archived successfully!
+
+ `;
+ const container = document.querySelector('.container');
+ container.prepend(alert);
+ setTimeout(() => alert.remove(), 3000);
+ } else {
+ const error = await response.json();
+ alert(error.message || 'Failed to archive reservation');
+ }
+ } catch (error) {
+ console.error('Error archiving reservation:', error);
+ alert('Failed to archive reservation');
+ }
+}
+
+// Set up event listeners
+function setupEventListeners() {
+ document.getElementById('locationFilter').addEventListener('change', filterAndDisplayReservations);
+ document.getElementById('statusFilter').addEventListener('change', filterAndDisplayReservations);
+ document.getElementById('logoutBtn').addEventListener('click', () => {
+ localStorage.clear();
+ window.location.href = '/index.html';
+ });
+
+ // Search functionality
+ const searchInput = document.getElementById('searchInput');
+ const clearSearch = document.getElementById('clearSearch');
+
+ searchInput.addEventListener('input', filterAndDisplayReservations);
+ searchInput.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') {
+ filterAndDisplayReservations();
+ }
+ });
+
+ clearSearch.addEventListener('click', () => {
+ searchInput.value = '';
+ filterAndDisplayReservations();
+ searchInput.focus();
+ });
+}
+
+// Auto refresh functionality
+let refreshInterval;
+let lastReservationsChecksum = '';
+
+function startAutoRefresh() {
+ // Refresh every 15 seconds for reservations (more frequent for status changes)
+ refreshInterval = setInterval(async () => {
+ await checkForReservationUpdates();
+ }, 15000);
+}
+
+function stopAutoRefresh() {
+ if (refreshInterval) {
+ clearInterval(refreshInterval);
+ }
+}
+
+async function checkForReservationUpdates() {
+ try {
+ const response = await fetch('/api/reservations', {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+
+ if (response.ok) {
+ const currentReservations = await response.json();
+
+ // Create a simple checksum of the reservations data
+ const currentChecksum = JSON.stringify(currentReservations.map(res => ({
+ id: res._id,
+ status: res.status,
+ quantity: res.quantity || 1
+ })));
+
+ // If reservations changed, refresh the display
+ if (lastReservationsChecksum && currentChecksum !== lastReservationsChecksum) {
+ reservations = currentReservations;
+ filterAndDisplayReservations();
+
+ // Show update notification
+ showUpdateNotification();
+ }
+
+ lastReservationsChecksum = currentChecksum;
+ }
+ } catch (error) {
+ console.error('Error checking for reservation updates:', error);
+ }
+}
+
+function showUpdateNotification() {
+ const notification = document.createElement('div');
+ notification.className = 'alert alert-info alert-dismissible fade show';
+ notification.innerHTML = `
+ Reservations updated!
+
+ `;
+ const container = document.querySelector('.container');
+ container.prepend(notification);
+ setTimeout(() => notification.remove(), 3000);
+}
+
+// Initialize the page when DOM is loaded
+document.addEventListener('DOMContentLoaded', initializePage);
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/admin.js b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/admin.js
new file mode 100644
index 0000000..983c23a
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/admin.js
@@ -0,0 +1,617 @@
+// Admin dashboard functionality
+let items = [];
+let reservations = [];
+let currentView = 'grid'; // Default view mode
+
+// Check authentication
+function checkAuth() {
+ const token = localStorage.getItem('token');
+ const role = localStorage.getItem('userRole');
+
+ if (!token || role !== 'admin') {
+ window.location.href = '/index.html';
+ }
+}
+
+// Initialize page
+async function initializePage() {
+ try {
+ checkAuth();
+ await loadItems();
+ displayUserInfo();
+ setupEventListeners(); // Move this after displayUserInfo to ensure DOM is ready
+ startAutoRefresh();
+ console.log('Page initialization complete');
+ } catch (error) {
+ console.error('Error during page initialization:', error);
+ throw error; // Re-throw to be caught by the outer try-catch
+ }
+}
+
+// Reload content when language changes
+function reloadContent() {
+ displayItems();
+}
+
+// Make reloadContent available globally for translation manager
+window.reloadContent = reloadContent;
+
+// Display user info
+function displayUserInfo() {
+ const username = localStorage.getItem('username');
+ document.getElementById('userInfo').textContent = `Admin: ${username}`;
+}
+
+// Load items from server
+async function loadItems() {
+ try {
+ console.log('Loading items...');
+ const response = await fetch('/api/items', {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+
+ const data = await response.json();
+ console.log('Received items:', data);
+
+ if (!response.ok) {
+ throw new Error(data.message || 'Failed to load items');
+ }
+
+ items = data;
+ displayItems();
+ } catch (error) {
+ console.error('Error loading items:', error);
+ alert('Failed to load items: ' + error.message);
+ }
+}
+
+// Filter items based on search term
+function getFilteredItems() {
+ const searchTerm = document.getElementById('searchInput') ?
+ document.getElementById('searchInput').value.toLowerCase() : '';
+
+ if (searchTerm.trim() === '') {
+ return items;
+ }
+
+ return items.filter(item => {
+ const translationManager = window.ensureTranslationManager ? window.ensureTranslationManager() : null;
+ if (translationManager) {
+ const localizedItem = translationManager.getLocalizedItem(item);
+ return localizedItem.name.toLowerCase().includes(searchTerm) ||
+ (localizedItem.description && localizedItem.description.toLowerCase().includes(searchTerm)) ||
+ item.location.toLowerCase().includes(searchTerm);
+ } else if (window.getItemDisplayText) {
+ const currentLang = localStorage.getItem('language') || 'nl';
+ const name = window.getItemDisplayText(item, 'name', currentLang);
+ const description = window.getItemDisplayText(item, 'description', currentLang);
+ return name.toLowerCase().includes(searchTerm) ||
+ description.toLowerCase().includes(searchTerm) ||
+ item.location.toLowerCase().includes(searchTerm);
+ } else {
+ // Fallback for when translation manager isn't loaded yet (default to Dutch)
+ const name = item.name?.nl || item.name?.en || item.name || '';
+ const description = item.description?.nl || item.description?.en || item.description || '';
+ return name.toLowerCase().includes(searchTerm) ||
+ description.toLowerCase().includes(searchTerm) ||
+ item.location.toLowerCase().includes(searchTerm);
+ }
+ });
+}
+
+// Display items based on current view mode
+function displayItems() {
+ if (currentView === 'grid') {
+ displayGridView();
+ } else {
+ displayListView();
+ }
+}
+
+// Display items in grid view
+function displayGridView() {
+ const itemsGrid = document.getElementById('itemsGrid');
+ document.getElementById('itemsList').classList.add('d-none');
+ itemsGrid.classList.remove('d-none');
+
+ const filteredItems = getFilteredItems();
+ itemsGrid.innerHTML = filteredItems.map(item => {
+ // Use multiple fallback strategies
+ const translationManager = window.ensureTranslationManager ? window.ensureTranslationManager() : null;
+ let localizedItem;
+
+ if (translationManager) {
+ localizedItem = translationManager.getLocalizedItem(item);
+ } else if (window.getItemDisplayText) {
+ // Use debug helper fallback
+ const currentLang = localStorage.getItem('language') || 'nl';
+ localizedItem = {
+ ...item,
+ name: window.getItemDisplayText(item, 'name', currentLang),
+ description: window.getItemDisplayText(item, 'description', currentLang)
+ };
+ } else {
+ // Final fallback
+ localizedItem = {
+ ...item,
+ name: (typeof item.name === 'object') ? (item.name?.nl || item.name?.en || 'Onbekend Artikel') : (item.name || 'Onbekend Artikel'),
+ description: (typeof item.description === 'object') ? (item.description?.nl || item.description?.en || 'Geen beschrijving beschikbaar') : (item.description || 'Geen beschrijving beschikbaar')
+ };
+ }
+
+ return `
+
+
+

+
+
${localizedItem.name}
+
${localizedItem.description || 'No description available'}
+
Location: ${item.location}
+
Quantity: ${item.quantity}
+
Reserved: ${item.reserved || 0}
+
+
+
+
+ `;
+ }).join('');
+}
+
+// Display items in list view
+function displayListView() {
+ const itemsList = document.getElementById('itemsList');
+ const itemsGrid = document.getElementById('itemsGrid');
+ itemsGrid.classList.add('d-none');
+ itemsList.classList.remove('d-none');
+
+ const itemsListBody = document.getElementById('itemsListBody');
+ const filteredItems = getFilteredItems();
+ itemsListBody.innerHTML = filteredItems.map(item => {
+ // Use multiple fallback strategies
+ const translationManager = window.ensureTranslationManager ? window.ensureTranslationManager() : null;
+ let localizedItem;
+
+ if (translationManager) {
+ localizedItem = translationManager.getLocalizedItem(item);
+ } else if (window.getItemDisplayText) {
+ // Use debug helper fallback
+ const currentLang = localStorage.getItem('language') || 'nl';
+ localizedItem = {
+ ...item,
+ name: window.getItemDisplayText(item, 'name', currentLang),
+ description: window.getItemDisplayText(item, 'description', currentLang)
+ };
+ } else {
+ // Final fallback
+ localizedItem = {
+ ...item,
+ name: (typeof item.name === 'object') ? (item.name?.nl || item.name?.en || 'Onbekend Artikel') : (item.name || 'Onbekend Artikel'),
+ description: (typeof item.description === 'object') ? (item.description?.nl || item.description?.en || 'Geen beschrijving beschikbaar') : (item.description || 'Geen beschrijving beschikbaar')
+ };
+ }
+
+ return `
+
+  |
+ ${localizedItem.name} |
+ ${localizedItem.description || 'No description available'} |
+ ${item.location} |
+ ${item.quantity} |
+ ${item.reserved || 0} |
+
+
+
+ |
+
+ `;
+ }).join('');
+}
+
+// Load reservations
+async function loadReservations() {
+ try {
+ const response = await fetch('/api/reservations', {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+ reservations = await response.json();
+ displayReservations();
+ } catch (error) {
+ console.error('Error loading reservations:', error);
+ alert('Failed to load reservations');
+ }
+}
+
+// Display reservations in table
+function displayReservations() {
+ const reservationsList = document.getElementById('reservationsList');
+ reservationsList.innerHTML = reservations.map(reservation => `
+
+ | ${reservation.studentName} |
+ ${reservation.itemName} |
+ ${reservation.location} |
+ ${new Date(reservation.reservedDate).toLocaleDateString()} |
+ ${reservation.status} |
+
+ ${reservation.status === 'PENDING' ? `
+
+
+ ` : ''}
+ |
+
+ `).join('');
+}
+
+
+// Delete item
+async function deleteItem(itemId) {
+ if (!confirm('Are you sure you want to delete this item?')) return;
+
+ try {
+ const response = await fetch(`/api/items/${itemId}`, {
+ method: 'DELETE',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+
+ if (response.ok) {
+ await loadItems();
+ } else {
+ const error = await response.json();
+ alert(error.message || 'Failed to delete item');
+ }
+ } catch (error) {
+ console.error('Error deleting item:', error);
+ alert('Failed to delete item');
+ }
+}
+
+// Update reservation status
+async function updateReservation(reservationId, status) {
+ try {
+ const response = await fetch(`/api/reservations/${reservationId}`, {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ },
+ body: JSON.stringify({ status })
+ });
+
+ if (response.ok) {
+ await loadReservations();
+ await loadItems(); // Refresh items to update quantities
+ } else {
+ const error = await response.json();
+ alert(error.message || 'Failed to update reservation');
+ }
+ } catch (error) {
+ console.error('Error updating reservation:', error);
+ alert('Failed to update reservation');
+ }
+}
+
+// Edit item functionality
+let editModal = null;
+
+// Initialize Bootstrap modal when DOM is loaded
+function initializeModal() {
+ const modalElement = document.getElementById('editItemModal');
+ if (!modalElement) {
+ console.error('Modal element not found in the DOM');
+ return;
+ }
+ try {
+ editModal = new bootstrap.Modal(modalElement);
+ console.log('Modal initialized successfully');
+ } catch (error) {
+ console.error('Error initializing modal:', error);
+ }
+}
+
+async function editItem(itemId) {
+ try {
+ console.log('Fetching item with ID:', itemId); // Debug log
+ const response = await fetch(`/api/items/${itemId}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+
+ const data = await response.json();
+ console.log('Response data:', data); // Debug log
+
+ if (!response.ok) {
+ console.error('Server error:', data);
+ throw new Error(data.message || 'Failed to fetch item');
+ }
+
+ if (!data || !data._id) {
+ throw new Error('Invalid item data received');
+ }
+
+ document.getElementById('editItemId').value = data._id;
+ document.getElementById('editItemName').value = data.name;
+ document.getElementById('editItemDescription').value = data.description || '';
+ document.getElementById('editItemLocation').value = data.location;
+ document.getElementById('editItemQuantity').value = data.quantity;
+
+ // Show current image if it exists
+ const imagePreview = document.getElementById('editImagePreview');
+ const removeButton = document.getElementById('editRemoveImage');
+ imagePreview.innerHTML = '';
+
+ if (data.imageUrl) {
+ const img = document.createElement('img');
+ img.src = data.imageUrl;
+ img.classList.add('modal-item-image');
+ imagePreview.appendChild(img);
+ removeButton.style.display = 'block';
+ imagePreview.dataset.currentImageUrl = data.imageUrl;
+ } else {
+ removeButton.style.display = 'none';
+ imagePreview.dataset.currentImageUrl = '';
+ }
+
+ // Show the modal
+ if (!editModal) {
+ console.log('Modal not initialized, attempting to initialize now');
+ initializeModal();
+ }
+
+ if (editModal) {
+ editModal.show();
+ } else {
+ throw new Error('Could not initialize modal. Please try again.');
+ }
+ } catch (error) {
+ console.error('Error fetching item:', error);
+ alert('Error loading item details: ' + error.message);
+ }
+}
+
+async function submitEditItem() {
+ const itemId = document.getElementById('editItemId').value;
+ const formData = new FormData();
+ formData.append('name', document.getElementById('editItemName').value);
+ formData.append('description', document.getElementById('editItemDescription').value);
+ formData.append('location', document.getElementById('editItemLocation').value);
+ formData.append('quantity', document.getElementById('editItemQuantity').value);
+
+ const imagePreview = document.getElementById('editImagePreview');
+ const imageFile = document.getElementById('editItemImage').files[0];
+ const currentImageUrl = imagePreview.dataset.currentImageUrl;
+
+ try {
+ // Handle image update
+ if (imageFile) {
+ // Upload new image
+ const imageFormData = new FormData();
+ imageFormData.append('image', imageFile);
+
+ const uploadResponse = await fetch('/api/upload', {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ },
+ body: imageFormData
+ });
+
+ if (!uploadResponse.ok) {
+ throw new Error('Failed to upload image');
+ }
+
+ const { imageUrl } = await uploadResponse.json();
+ formData.append('imageUrl', imageUrl);
+ } else if (!currentImageUrl) {
+ // If no new image and no current image, explicitly set imageUrl to null
+ formData.append('imageUrl', '');
+ }
+ } catch (error) {
+ console.error('Error uploading image:', error);
+ alert('Failed to upload image');
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/items/${itemId}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ },
+ body: JSON.stringify(Object.fromEntries(formData))
+ });
+
+ if (!response.ok) {
+ throw new Error('Update failed');
+ }
+
+ await loadItems(); // Refresh the items list
+ bootstrap.Modal.getInstance(document.getElementById('editItemModal')).hide();
+ alert('Item updated successfully');
+ } catch (error) {
+ console.error('Error updating item:', error);
+ alert('Error updating item');
+ }
+}
+
+// Set up event listeners
+function setupEventListeners() {
+ // Common elements
+ const logoutBtn = document.getElementById('logoutBtn');
+ if (logoutBtn) {
+ logoutBtn.addEventListener('click', () => {
+ localStorage.clear();
+ window.location.href = '/index.html';
+ });
+ }
+
+ // View mode toggle
+ const viewModeBtns = document.querySelectorAll('.view-mode-btn');
+ if (viewModeBtns.length > 0) {
+ viewModeBtns.forEach(btn => {
+ btn.addEventListener('click', function() {
+ document.querySelectorAll('.view-mode-btn').forEach(b => b.classList.remove('active'));
+ this.classList.add('active');
+ currentView = this.dataset.mode;
+ displayItems();
+ });
+ });
+ }
+
+ // Search functionality
+ const searchInput = document.getElementById('searchInput');
+ const clearSearch = document.getElementById('clearSearch');
+
+ if (searchInput) {
+ searchInput.addEventListener('input', displayItems);
+ searchInput.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') {
+ displayItems();
+ }
+ });
+ }
+
+ if (clearSearch) {
+ clearSearch.addEventListener('click', () => {
+ if (searchInput) {
+ searchInput.value = '';
+ displayItems();
+ searchInput.focus();
+ }
+ });
+ }
+
+ // Edit form elements
+ const editItemImage = document.getElementById('editItemImage');
+ if (editItemImage) {
+ editItemImage.addEventListener('change', function(e) {
+ const file = e.target.files[0];
+ const imagePreview = document.getElementById('editImagePreview');
+ const removeButton = document.getElementById('editRemoveImage');
+
+ if (file) {
+ const reader = new FileReader();
+ reader.onload = function(e) {
+ imagePreview.innerHTML = '';
+ const img = document.createElement('img');
+ img.src = e.target.result;
+ img.classList.add('modal-item-image');
+ imagePreview.appendChild(img);
+ removeButton.style.display = 'block';
+ };
+ reader.readAsDataURL(file);
+ } else {
+ imagePreview.innerHTML = '';
+ if (!imagePreview.dataset.currentImageUrl) {
+ removeButton.style.display = 'none';
+ }
+ }
+ });
+ }
+
+ // Remove image button
+ const editRemoveImage = document.getElementById('editRemoveImage');
+ if (editRemoveImage) {
+ editRemoveImage.addEventListener('click', function() {
+ const imagePreview = document.getElementById('editImagePreview');
+ const imageInput = document.getElementById('editItemImage');
+ const removeButton = document.getElementById('editRemoveImage');
+
+ imagePreview.innerHTML = '';
+ imageInput.value = '';
+ imagePreview.dataset.currentImageUrl = '';
+ removeButton.style.display = 'none';
+ });
+ }
+}
+
+// Initialize the page when DOM is loaded
+document.addEventListener('DOMContentLoaded', async () => {
+ try {
+ console.log('Initializing page...');
+ await initializePage();
+ console.log('Page initialized');
+
+ // Ensure modal is initialized after Bootstrap is loaded
+ setTimeout(() => {
+ console.log('Initializing modal...');
+ initializeModal();
+ console.log('Modal initialized');
+ }, 100);
+ } catch (error) {
+ console.error('Error during page initialization:', error);
+ alert('Error initializing page: ' + error.message);
+ }
+});
+
+// Auto refresh functionality
+let refreshInterval;
+let lastItemsChecksum = '';
+
+function startAutoRefresh() {
+ // Refresh every 30 seconds
+ refreshInterval = setInterval(async () => {
+ await checkForUpdates();
+ }, 30000);
+}
+
+function stopAutoRefresh() {
+ if (refreshInterval) {
+ clearInterval(refreshInterval);
+ }
+}
+
+async function checkForUpdates() {
+ try {
+ const response = await fetch('/api/items', {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+
+ if (response.ok) {
+ const currentItems = await response.json();
+
+ // Create a simple checksum of the items data
+ const currentChecksum = JSON.stringify(currentItems.map(item => ({
+ id: item._id,
+ quantity: item.quantity,
+ reserved: item.reserved || 0
+ })));
+
+ // If items changed, refresh the display
+ if (lastItemsChecksum && currentChecksum !== lastItemsChecksum) {
+ items = currentItems;
+ displayItems();
+
+ // Show update notification
+ showUpdateNotification();
+ }
+
+ lastItemsChecksum = currentChecksum;
+ }
+ } catch (error) {
+ console.error('Error checking for updates:', error);
+ }
+}
+
+function showUpdateNotification() {
+ const notification = document.createElement('div');
+ notification.className = 'alert alert-info alert-dismissible fade show';
+ notification.innerHTML = `
+ Items updated!
+
+ `;
+ const container = document.querySelector('.container');
+ container.prepend(notification);
+ setTimeout(() => notification.remove(), 3000);
+}
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/auth.js b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/auth.js
new file mode 100644
index 0000000..f83b00e
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/auth.js
@@ -0,0 +1,36 @@
+// Authentication related functions
+document.getElementById('loginForm').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const username = document.getElementById('username').value.toLowerCase(); // Convert to lowercase
+ const password = document.getElementById('password').value;
+
+ try {
+ const response = await fetch('/api/auth/login', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ username, password })
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ localStorage.setItem('token', data.token);
+ localStorage.setItem('userRole', data.role);
+ localStorage.setItem('username', data.username.toLowerCase());
+
+ // Redirect based on role
+ if (data.role === 'admin') {
+ window.location.href = '/admin.html';
+ } else {
+ window.location.href = '/student.html';
+ }
+ } else {
+ alert(data.message || 'Login failed');
+ }
+ } catch (error) {
+ console.error('Login error:', error);
+ alert('An error occurred during login');
+ }
+});
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/debug-helper.js b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/debug-helper.js
new file mode 100644
index 0000000..faa69fd
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/debug-helper.js
@@ -0,0 +1,50 @@
+// Debug helper for translation issues
+console.log('Debug helper loaded');
+
+// Function to check item structure
+window.debugItem = function(item) {
+ console.log('Item structure:', item);
+ console.log('Item name type:', typeof item.name);
+ console.log('Item name value:', item.name);
+ console.log('Item description type:', typeof item.description);
+ console.log('Item description value:', item.description);
+
+ if (window.translationManager) {
+ console.log('Translation manager exists');
+ const localized = window.translationManager.getLocalizedItem(item);
+ console.log('Localized item:', localized);
+ } else {
+ console.log('Translation manager not available');
+ }
+};
+
+// Function to ensure translation manager
+window.ensureTranslationManager = function() {
+ if (!window.translationManager) {
+ console.log('Creating new translation manager instance');
+ if (window.TranslationManager) {
+ window.translationManager = new window.TranslationManager();
+ } else {
+ console.error('TranslationManager class not available');
+ return null;
+ }
+ }
+ return window.translationManager;
+};
+
+// Enhanced fallback function for items
+window.getItemDisplayText = function(item, field, language = 'nl') {
+ if (!item) return field === 'name' ? 'Onbekend Artikel' : 'Geen beschrijving beschikbaar';
+
+ const value = item[field];
+
+ if (typeof value === 'object' && value !== null) {
+ return value[language] || value.nl || value.en || (field === 'name' ? 'Onbekend Artikel' : 'Geen beschrijving beschikbaar');
+ } else if (typeof value === 'string') {
+ return value;
+ } else {
+ return field === 'name' ? 'Onbekend Artikel' : 'Geen beschrijving beschikbaar';
+ }
+};
+
+console.log('Debug helper functions loaded');
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/register.js b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/register.js
new file mode 100644
index 0000000..fbe433a
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/register.js
@@ -0,0 +1,60 @@
+// Registration form handling
+document.getElementById('registrationForm').addEventListener('submit', async (e) => {
+ e.preventDefault();
+
+ const username = document.getElementById('username').value.toLowerCase(); // Convert to lowercase
+ const email = document.getElementById('email').value;
+ const password = document.getElementById('password').value;
+ const confirmPassword = document.getElementById('confirmPassword').value;
+
+ // Validate username (only allow alphanumeric and underscores)
+ const usernameRegex = /^[a-z0-9_]+$/;
+ if (!usernameRegex.test(username)) {
+ alert('Username can only contain letters, numbers, and underscores');
+ return;
+ }
+
+ // Validate password match
+ if (password !== confirmPassword) {
+ alert('Passwords do not match!');
+ return;
+ }
+
+ // Validate email format
+ const emailRegex = /^\d+@vistacollege\.nl$/;
+ if (!emailRegex.test(email)) {
+ alert('Email must be in the format: studentnumber@vistacollege.nl');
+ return;
+ }
+
+ try {
+ const response = await fetch('/api/auth/register', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ username,
+ email,
+ password
+ })
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ // Store the token and user info
+ localStorage.setItem('token', data.token);
+ localStorage.setItem('userRole', data.role);
+ localStorage.setItem('username', data.username.toLowerCase());
+
+ // Redirect to student dashboard
+ window.location.href = '/student.html';
+ } else {
+ alert(data.message || 'Registration failed');
+ }
+ } catch (error) {
+ console.error('Registration error:', error);
+ alert('An error occurred during registration');
+ }
+});
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/student-reservations.js b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/student-reservations.js
new file mode 100644
index 0000000..0b9dedc
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/student-reservations.js
@@ -0,0 +1,269 @@
+// Student reservations page functionality
+let reservations = [];
+
+// Check authentication
+function checkAuth() {
+ const token = localStorage.getItem('token');
+ const role = localStorage.getItem('userRole');
+
+ if (!token || role !== 'student') {
+ window.location.href = '/index.html';
+ }
+}
+
+// Initialize page
+async function initializePage() {
+ checkAuth();
+ await loadReservations();
+ setupEventListeners();
+ displayUserInfo();
+ startAutoRefresh();
+}
+
+// Reload content when language changes
+function reloadContent() {
+ displayReservations();
+}
+
+// Make reloadContent available globally for translation manager
+window.reloadContent = reloadContent;
+
+// Display user info
+function displayUserInfo() {
+ const username = localStorage.getItem('username');
+ document.getElementById('userInfo').textContent = `Student: ${username}`;
+}
+
+// Load and filter reservations
+async function loadReservations() {
+ try {
+ const response = await fetch('/api/reservations/my', {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.message || `HTTP ${response.status}`);
+ }
+
+ reservations = await response.json();
+ filterAndDisplayReservations();
+ } catch (error) {
+ console.error('Error loading reservations:', error);
+ // Display a user-friendly error message
+ const reservationsList = document.getElementById('reservationsList');
+ reservationsList.innerHTML = `
+
+
+
+ Failed to load reservations: ${error.message}
+ Please try refreshing the page
+ |
+
+ `;
+ }
+}
+
+// Filter and display reservations
+function filterAndDisplayReservations() {
+ const locationFilter = document.getElementById('locationFilter').value;
+ const statusFilter = document.getElementById('statusFilter').value;
+ const searchTerm = document.getElementById('searchInput').value.toLowerCase();
+
+ const filteredReservations = reservations.filter(reservation => {
+ const locationMatch = locationFilter === 'all' || reservation.location === locationFilter;
+ const statusMatch = statusFilter === 'all' || reservation.status === statusFilter;
+ const searchMatch = searchTerm === '' ||
+ reservation.itemName.toLowerCase().includes(searchTerm) ||
+ reservation.location.toLowerCase().includes(searchTerm);
+ return locationMatch && statusMatch && searchMatch;
+ });
+
+ const reservationsList = document.getElementById('reservationsList');
+ reservationsList.innerHTML = filteredReservations.map(reservation => `
+
+ | ${reservation.itemName} |
+ ${reservation.quantity || 1} |
+ ${reservation.location} |
+ ${new Date(reservation.reservedDate).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric'
+ })} |
+ ${reservation.status} |
+
+ ${reservation.status === 'PENDING' ? `
+
+ ` : reservation.status === 'APPROVED' ? `
+
+ ` : reservation.status === 'REJECTED' ? `
+ Rejected
+ ` : reservation.status === 'RETURN_PENDING' ? `
+ Return Pending Approval
+ ` : reservation.status === 'RETURNED' ? `
+ Returned
+ ` : ''}
+ |
+
+ `).join('');
+}
+
+// Delete (cancel) reservation
+async function deleteReservation(reservationId) {
+ if (!confirm('Weet je zeker dat je deze reservering wilt annuleren?')) return;
+
+ try {
+ const response = await fetch(`/api/reservations/${reservationId}`, {
+ method: 'DELETE',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+
+ if (response.ok) {
+ await loadReservations();
+ } else {
+ const error = await response.json();
+ alert(error.message || 'Kon reservering niet annuleren');
+ }
+ } catch (error) {
+ console.error('Error canceling reservation:', error);
+ alert('Kon reservering niet annuleren');
+ }
+}
+
+// Return reservation (mark as returned)
+async function returnReservation(reservationId) {
+ if (!confirm('Weet je zeker dat je retour wilt aanvragen voor dit artikel? Een admin moet de retour goedkeuren.')) return;
+
+ try {
+ const response = await fetch(`/api/reservations/${reservationId}`, {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ },
+ body: JSON.stringify({ status: 'RETURN_PENDING' })
+ });
+
+ if (response.ok) {
+ // Successfully requested return, reload reservations
+ await loadReservations();
+ // Show success message
+ const alert = document.createElement('div');
+ alert.className = 'alert alert-success alert-dismissible fade show';
+ alert.innerHTML = `
+ Retour succesvol aangevraagd! Een admin zal je verzoek beoordelen.
+
+ `;
+ const container = document.querySelector('.container');
+ container.prepend(alert);
+ setTimeout(() => alert.remove(), 5000);
+ } else {
+ const error = await response.json();
+ throw new Error(error.message || 'Kon retour niet aanvragen');
+ }
+ } catch (error) {
+ console.error('Error requesting return:', error);
+ alert(`Kon retour niet aanvragen: ${error.message}`);
+ }
+}
+
+// Set up event listeners
+function setupEventListeners() {
+ document.getElementById('locationFilter').addEventListener('change', filterAndDisplayReservations);
+ document.getElementById('statusFilter').addEventListener('change', filterAndDisplayReservations);
+ document.getElementById('logoutBtn').addEventListener('click', () => {
+ localStorage.clear();
+ window.location.href = '/index.html';
+ });
+
+ // Search functionality
+ const searchInput = document.getElementById('searchInput');
+ const clearSearch = document.getElementById('clearSearch');
+
+ searchInput.addEventListener('input', filterAndDisplayReservations);
+ searchInput.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') {
+ filterAndDisplayReservations();
+ }
+ });
+
+ clearSearch.addEventListener('click', () => {
+ searchInput.value = '';
+ filterAndDisplayReservations();
+ searchInput.focus();
+ });
+}
+
+// Auto refresh functionality
+let refreshInterval;
+let lastReservationsChecksum = '';
+
+function startAutoRefresh() {
+ // Refresh every 15 seconds for reservations (more frequent for status changes)
+ refreshInterval = setInterval(async () => {
+ await checkForReservationUpdates();
+ }, 15000);
+}
+
+function stopAutoRefresh() {
+ if (refreshInterval) {
+ clearInterval(refreshInterval);
+ }
+}
+
+async function checkForReservationUpdates() {
+ try {
+ const response = await fetch('/api/reservations/my', {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+
+ if (response.ok) {
+ const currentReservations = await response.json();
+
+ // Create a simple checksum of the reservations data
+ const currentChecksum = JSON.stringify(currentReservations.map(res => ({
+ id: res._id,
+ status: res.status,
+ quantity: res.quantity
+ })));
+
+ // If reservations changed, refresh the display
+ if (lastReservationsChecksum && currentChecksum !== lastReservationsChecksum) {
+ reservations = currentReservations;
+ filterAndDisplayReservations();
+
+ // Show update notification
+ showUpdateNotification();
+ }
+
+ lastReservationsChecksum = currentChecksum;
+ }
+ } catch (error) {
+ console.error('Error checking for reservation updates:', error);
+ }
+}
+
+function showUpdateNotification() {
+ const notification = document.createElement('div');
+ notification.className = 'alert alert-info alert-dismissible fade show';
+ notification.innerHTML = `
+ Reservations updated!
+
+ `;
+ const container = document.querySelector('.container');
+ container.prepend(notification);
+ setTimeout(() => notification.remove(), 3000);
+}
+
+// Initialize the page when DOM is loaded
+document.addEventListener('DOMContentLoaded', initializePage);
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/student.js b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/student.js
new file mode 100644
index 0000000..06b7e83
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/student.js
@@ -0,0 +1,404 @@
+// Student dashboard functionality
+let items = [];
+
+// Check authentication
+function checkAuth() {
+ const token = localStorage.getItem('token');
+ const role = localStorage.getItem('userRole');
+
+ if (!token || role !== 'student') {
+ window.location.href = '/index.html';
+ }
+}
+
+// Initialize page
+async function initializePage() {
+ checkAuth();
+ await loadItems();
+ setupEventListeners();
+ displayUserInfo();
+ initializeModal();
+ startAutoRefresh();
+}
+
+// Reload content when language changes
+function reloadContent() {
+ displayItems();
+ loadMyReservations();
+}
+
+// Make reloadContent available globally for translation manager
+window.reloadContent = reloadContent;
+
+// Display user info
+function displayUserInfo() {
+ const username = localStorage.getItem('username');
+ document.getElementById('userInfo').textContent = `Student: ${username}`;
+}
+
+// Load items from server
+async function loadItems() {
+ try {
+ const response = await fetch('/api/items', {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+ items = await response.json();
+ displayItems();
+ } catch (error) {
+ console.error('Error loading items:', error);
+ alert('Kon artikelen niet laden');
+ }
+}
+
+let itemDetailsModal = null;
+let currentItem = null;
+
+// Initialize Bootstrap modal
+function initializeModal() {
+ const modalElement = document.getElementById('itemDetailsModal');
+ itemDetailsModal = new bootstrap.Modal(modalElement);
+
+ // Set up modal reserve button
+ document.getElementById('modalReserveButton').addEventListener('click', () => {
+ if (currentItem) {
+ const quantity = parseInt(document.getElementById('reserveQuantity').value);
+ reserveItem(currentItem._id, quantity);
+ }
+ });
+}
+
+// Show item details in modal
+function showItemDetails(item) {
+ currentItem = item;
+ const availableQuantity = item.quantity - (item.reserved || 0);
+
+ // Set modal content
+ document.getElementById('modalItemImage').src = item.imageUrl || '/images/default-item.png';
+ document.getElementById('modalItemName').textContent = item.name;
+ document.getElementById('modalItemDescription').textContent = item.description || 'No description available';
+ document.getElementById('modalItemLocation').textContent = item.location;
+ document.getElementById('modalItemQuantity').textContent = availableQuantity;
+
+ // Populate quantity select
+ const quantitySelect = document.getElementById('reserveQuantity');
+ quantitySelect.innerHTML = '';
+ for (let i = 1; i <= availableQuantity; i++) {
+ const option = document.createElement('option');
+ option.value = i;
+ option.textContent = i;
+ quantitySelect.appendChild(option);
+ }
+
+ // Show/hide reserve button and quantity select based on availability
+ const reserveButton = document.getElementById('modalReserveButton');
+ const quantityGroup = document.getElementById('quantitySelectGroup');
+ if (availableQuantity > 0) {
+ reserveButton.style.display = 'block';
+ quantityGroup.style.display = 'block';
+ reserveButton.disabled = false;
+ } else {
+ reserveButton.style.display = 'none';
+ quantityGroup.style.display = 'none';
+ }
+
+ itemDetailsModal.show();
+}
+
+// Track current view mode
+let currentViewMode = localStorage.getItem('studentViewMode') || 'grid';
+
+// Display items based on current view mode
+function displayItems() {
+ const locationFilter = document.getElementById('locationFilter').value;
+ const searchTerm = document.getElementById('searchInput').value.toLowerCase();
+
+ let filteredItems = items;
+
+ // Apply location filter
+ if (locationFilter !== 'all') {
+ filteredItems = filteredItems.filter(item => item.location === locationFilter);
+ }
+
+ // Apply search filter with translation support
+ if (searchTerm.trim() !== '') {
+ filteredItems = filteredItems.filter(item => {
+ const translationManager = window.ensureTranslationManager ? window.ensureTranslationManager() : null;
+ if (translationManager) {
+ const localizedItem = translationManager.getLocalizedItem(item);
+ return localizedItem.name.toLowerCase().includes(searchTerm) ||
+ (localizedItem.description && localizedItem.description.toLowerCase().includes(searchTerm));
+ } else if (window.getItemDisplayText) {
+ const currentLang = localStorage.getItem('language') || 'nl';
+ const name = window.getItemDisplayText(item, 'name', currentLang);
+ const description = window.getItemDisplayText(item, 'description', currentLang);
+ return name.toLowerCase().includes(searchTerm) || description.toLowerCase().includes(searchTerm);
+ } else {
+ // Fallback for when translation manager isn't loaded yet (default to Dutch)
+ const name = item.name?.nl || item.name?.en || item.name || '';
+ const description = item.description?.nl || item.description?.en || item.description || '';
+ return name.toLowerCase().includes(searchTerm) || description.toLowerCase().includes(searchTerm);
+ }
+ });
+ }
+
+ // Update view mode buttons active state
+ const viewButtons = document.querySelectorAll('.view-mode-btn');
+ viewButtons.forEach(btn => {
+ btn.classList.toggle('active', btn.getAttribute('data-mode') === currentViewMode);
+ });
+
+ // Show/hide appropriate containers
+ document.getElementById('itemsGrid').classList.toggle('d-none', currentViewMode !== 'grid');
+ document.getElementById('itemsList').classList.toggle('d-none', currentViewMode !== 'list');
+
+ if (currentViewMode === 'grid') {
+ // Display items in grid view
+ const gridContainer = document.getElementById('itemsGrid');
+ gridContainer.innerHTML = filteredItems.map(item => {
+ // Use multiple fallback strategies
+ const translationManager = window.ensureTranslationManager ? window.ensureTranslationManager() : null;
+ let localizedItem;
+
+ if (translationManager) {
+ localizedItem = translationManager.getLocalizedItem(item);
+ } else if (window.getItemDisplayText) {
+ // Use debug helper fallback
+ const currentLang = localStorage.getItem('language') || 'nl';
+ localizedItem = {
+ ...item,
+ name: window.getItemDisplayText(item, 'name', currentLang),
+ description: window.getItemDisplayText(item, 'description', currentLang)
+ };
+ } else {
+ // Final fallback
+ localizedItem = {
+ ...item,
+ name: (typeof item.name === 'object') ? (item.name?.nl || item.name?.en || 'Onbekend Artikel') : (item.name || 'Onbekend Artikel'),
+ description: (typeof item.description === 'object') ? (item.description?.nl || item.description?.en || 'Geen beschrijving beschikbaar') : (item.description || 'Geen beschrijving beschikbaar')
+ };
+ }
+
+ return `
+
+
+

+
+
${localizedItem.name}
+
${localizedItem.description || 'No description available'}
+
+ Location: ${item.location}
+ Available: ${item.quantity - (item.reserved || 0)}
+
+ ${item.quantity - (item.reserved || 0) > 0 ?
+ '
Available' :
+ '
Not Available'
+ }
+
+
+
+ `;
+ }).join('');
+ } else {
+ // Display items in list view
+ const itemsListBody = document.getElementById('itemsListBody');
+ itemsListBody.innerHTML = filteredItems.map(item => {
+ // Use multiple fallback strategies
+ const translationManager = window.ensureTranslationManager ? window.ensureTranslationManager() : null;
+ let localizedItem;
+
+ if (translationManager) {
+ localizedItem = translationManager.getLocalizedItem(item);
+ } else if (window.getItemDisplayText) {
+ // Use debug helper fallback
+ const currentLang = localStorage.getItem('language') || 'nl';
+ localizedItem = {
+ ...item,
+ name: window.getItemDisplayText(item, 'name', currentLang),
+ description: window.getItemDisplayText(item, 'description', currentLang)
+ };
+ } else {
+ // Final fallback
+ localizedItem = {
+ ...item,
+ name: (typeof item.name === 'object') ? (item.name?.nl || item.name?.en || 'Onbekend Artikel') : (item.name || 'Onbekend Artikel'),
+ description: (typeof item.description === 'object') ? (item.description?.nl || item.description?.en || 'Geen beschrijving beschikbaar') : (item.description || 'Geen beschrijving beschikbaar')
+ };
+ }
+
+ return `
+
+  |
+ ${localizedItem.name} |
+ ${localizedItem.description || 'No description available'} |
+ ${item.location} |
+ ${item.quantity - (item.reserved || 0)} |
+
+ ${item.quantity - (item.reserved || 0) > 0 ?
+ 'Available' :
+ 'Not Available'
+ }
+ |
+
+ `;
+ }).join('');
+ }
+}
+
+// Load user's reservations
+async function loadMyReservations() {
+ try {
+ const response = await fetch('/api/reservations/my', {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+ myReservations = await response.json();
+ displayMyReservations();
+ } catch (error) {
+ console.error('Error loading reservations:', error);
+ alert('Kon reserveringen niet laden');
+ }
+}
+
+// Display user's reservations
+function displayMyReservations() {
+ const reservationsList = document.getElementById('reservationsList');
+ reservationsList.innerHTML = myReservations.map(reservation => `
+
+ | ${reservation.itemName} |
+ ${reservation.location} |
+ ${new Date(reservation.reservedDate).toLocaleDateString()} |
+ ${reservation.status} |
+
+ `).join('');
+}
+
+// Reserve an item
+async function reserveItem(itemId, quantity = 1) {
+ try {
+ const response = await fetch('/api/reservations', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ },
+ body: JSON.stringify({ itemId, quantity })
+ });
+
+ if (response.ok) {
+ await loadItems();
+ itemDetailsModal.hide();
+ // Redirect to reservations page after successful reservation
+ window.location.href = '/student-reservations.html';
+ } else {
+ const error = await response.json();
+ alert(error.message || 'Kon artikel niet reserveren');
+ }
+ } catch (error) {
+ console.error('Error reserving item:', error);
+ alert('Kon artikel niet reserveren');
+ }
+}
+
+// Switch view mode
+function switchViewMode(mode) {
+ currentViewMode = mode;
+ localStorage.setItem('studentViewMode', mode);
+ displayItems();
+}
+
+// Set up event listeners
+function setupEventListeners() {
+ document.getElementById('locationFilter').addEventListener('change', displayItems);
+ document.getElementById('logoutBtn').addEventListener('click', () => {
+ localStorage.clear();
+ window.location.href = '/index.html';
+ });
+
+ // Search functionality
+ const searchInput = document.getElementById('searchInput');
+ const clearSearch = document.getElementById('clearSearch');
+
+ searchInput.addEventListener('input', displayItems);
+ searchInput.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') {
+ displayItems();
+ }
+ });
+
+ clearSearch.addEventListener('click', () => {
+ searchInput.value = '';
+ displayItems();
+ searchInput.focus();
+ });
+
+ // View mode toggle listeners
+ document.querySelectorAll('.view-mode-btn').forEach(btn => {
+ btn.addEventListener('click', () => switchViewMode(btn.getAttribute('data-mode')));
+ });
+}
+
+// Auto refresh functionality
+let refreshInterval;
+let lastItemsChecksum = '';
+
+function startAutoRefresh() {
+ // Refresh every 30 seconds
+ refreshInterval = setInterval(async () => {
+ await checkForUpdates();
+ }, 30000);
+}
+
+function stopAutoRefresh() {
+ if (refreshInterval) {
+ clearInterval(refreshInterval);
+ }
+}
+
+async function checkForUpdates() {
+ try {
+ const response = await fetch('/api/items', {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+ const currentItems = await response.json();
+
+ // Create a simple checksum of the items data
+ const currentChecksum = JSON.stringify(currentItems.map(item => ({
+ id: item._id,
+ quantity: item.quantity,
+ reserved: item.reserved || 0
+ })));
+
+ // If items changed, refresh the display
+ if (lastItemsChecksum && currentChecksum !== lastItemsChecksum) {
+ items = currentItems;
+ displayItems();
+
+ // Show update notification
+ showUpdateNotification();
+ }
+
+ lastItemsChecksum = currentChecksum;
+ } catch (error) {
+ console.error('Error checking for updates:', error);
+ }
+}
+
+function showUpdateNotification() {
+ const notification = document.createElement('div');
+ notification.className = 'alert alert-info alert-dismissible fade show';
+ notification.innerHTML = `
+ Items updated!
+
+ `;
+ const container = document.querySelector('.container');
+ container.prepend(notification);
+ setTimeout(() => notification.remove(), 3000);
+}
+
+// Initialize the page when DOM is loaded
+document.addEventListener('DOMContentLoaded', initializePage);
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/translations.js b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/translations.js
new file mode 100644
index 0000000..ee102d7
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/js/translations.js
@@ -0,0 +1,357 @@
+// Translation utility for the warehouse management system
+
+const translations = {
+ en: {
+ // Navigation
+ 'school-warehouse': 'School Warehouse',
+ 'available-items': 'Available Items',
+ 'my-reservations': 'My Reservations',
+ 'logout': 'Logout',
+ 'inventory': 'Inventory',
+ 'add-new-item': 'Add New Item',
+ 'reservations': 'Reservations',
+
+ // Page titles
+ 'warehouse-dashboard-admin': 'Warehouse Dashboard - Admin',
+ 'warehouse-dashboard-student': 'Warehouse Dashboard - Student',
+ 'my-reservations-student': 'My Reservations - Student',
+ 'login': 'Login',
+ 'register': 'Register',
+
+ // Forms
+ 'username': 'Username',
+ 'password': 'Password',
+ 'remember-me': 'Remember Me',
+ 'no-account': "Don't have an account?",
+ 'student-registration': 'Student Registration',
+ 'confirm-password': 'Confirm Password',
+ 'already-account': 'Already have an account?',
+
+ // Search and filters
+ 'search-items': 'Search items...',
+ 'search-reservations': 'Search reservations...',
+ 'clear-search': 'Clear search',
+ 'all-locations': 'All Locations',
+ 'all-status': 'All Status',
+ 'filter-reservations': 'Filter Reservations',
+ 'location': 'Location',
+ 'status': 'Status',
+ 'search': 'Search',
+
+ // Table headers
+ 'image': 'Image',
+ 'item-name': 'Item Name',
+ 'description': 'Description',
+ 'quantity': 'Quantity',
+ 'quantity-available': 'Quantity Available',
+ 'action': 'Action',
+ 'actions': 'Actions',
+ 'reserved-date': 'Reserved Date',
+
+ // View modes
+ 'grid': 'Grid',
+ 'list': 'List',
+
+ // Status values
+ 'pending': 'Pending',
+ 'approved': 'Approved',
+ 'rejected': 'Rejected',
+ 'return-pending': 'Return Pending',
+ 'returned': 'Returned',
+
+ // Buttons and actions
+ 'reserve': 'Reserve',
+ 'cancel': 'Cancel',
+ 'return': 'Return',
+ 'approve': 'Approve',
+ 'reject': 'Reject',
+ 'edit': 'Edit',
+ 'delete': 'Delete',
+
+ // Messages
+ 'loading': 'Loading...',
+ 'loading-items': 'Loading items...',
+ 'failed-load-items': 'Failed to load items',
+ 'failed-load-reservations': 'Failed to load reservations',
+ 'failed-reserve-item': 'Failed to reserve item',
+ 'failed-cancel-reservation': 'Failed to cancel reservation',
+ 'failed-request-return': 'Failed to request return',
+ 'confirm-cancel-reservation': 'Are you sure you want to cancel this reservation?',
+ 'confirm-return-request': 'Are you sure you want to request return for this item? An admin will need to approve the return.',
+ 'return-request-success': 'Return requested successfully! An admin will review your request.',
+
+ // Item fallbacks
+ 'unknown-item': 'Unknown Item',
+ 'no-description': 'No description available',
+
+ // Management
+ 'inventory-management': 'Inventory Management',
+ 'reservation-management': 'Reservation Management'
+ },
+ nl: {
+ // Navigation
+ 'school-warehouse': 'School Magazijn',
+ 'available-items': 'Beschikbare Artikelen',
+ 'my-reservations': 'Mijn Reserveringen',
+ 'logout': 'Uitloggen',
+ 'inventory': 'Voorraad',
+ 'add-new-item': 'Nieuw Artikel Toevoegen',
+ 'reservations': 'Reserveringen',
+
+ // Page titles
+ 'warehouse-dashboard-admin': 'Magazijn Dashboard - Admin',
+ 'warehouse-dashboard-student': 'Magazijn Dashboard - Student',
+ 'my-reservations-student': 'Mijn Reserveringen - Student',
+ 'login': 'Inloggen',
+ 'register': 'Registreren',
+
+ // Forms
+ 'username': 'Gebruikersnaam',
+ 'password': 'Wachtwoord',
+ 'remember-me': 'Onthoud mij',
+ 'no-account': 'Geen account?',
+ 'student-registration': 'Student Registratie',
+ 'confirm-password': 'Bevestig Wachtwoord',
+ 'already-account': 'Al een account?',
+
+ // Search and filters
+ 'search-items': 'Zoek artikelen...',
+ 'search-reservations': 'Zoek reserveringen...',
+ 'clear-search': 'Zoekopdracht wissen',
+ 'all-locations': 'Alle Locaties',
+ 'all-status': 'Alle Statussen',
+ 'filter-reservations': 'Filter Reserveringen',
+ 'location': 'Locatie',
+ 'status': 'Status',
+ 'search': 'Zoeken',
+
+ // Table headers
+ 'image': 'Afbeelding',
+ 'item-name': 'Artikelnaam',
+ 'description': 'Beschrijving',
+ 'quantity': 'Hoeveelheid',
+ 'quantity-available': 'Beschikbare Hoeveelheid',
+ 'action': 'Actie',
+ 'actions': 'Acties',
+ 'reserved-date': 'Reserveringsdatum',
+
+ // View modes
+ 'grid': 'Raster',
+ 'list': 'Lijst',
+
+ // Status values
+ 'pending': 'In Behandeling',
+ 'approved': 'Goedgekeurd',
+ 'rejected': 'Afgewezen',
+ 'return-pending': 'Retour in Behandeling',
+ 'returned': 'Geretourneerd',
+
+ // Buttons and actions
+ 'reserve': 'Reserveren',
+ 'cancel': 'Annuleren',
+ 'return': 'Retourneren',
+ 'approve': 'Goedkeuren',
+ 'reject': 'Afwijzen',
+ 'edit': 'Bewerken',
+ 'delete': 'Verwijderen',
+
+ // Messages
+ 'loading': 'Laden...',
+ 'loading-items': 'Artikelen laden...',
+ 'failed-load-items': 'Kon artikelen niet laden',
+ 'failed-load-reservations': 'Kon reserveringen niet laden',
+ 'failed-reserve-item': 'Kon artikel niet reserveren',
+ 'failed-cancel-reservation': 'Kon reservering niet annuleren',
+ 'failed-request-return': 'Kon retour niet aanvragen',
+ 'confirm-cancel-reservation': 'Weet je zeker dat je deze reservering wilt annuleren?',
+ 'confirm-return-request': 'Weet je zeker dat je retour wilt aanvragen voor dit artikel? Een admin moet de retour goedkeuren.',
+ 'return-request-success': 'Retour succesvol aangevraagd! Een admin zal je verzoek beoordelen.',
+
+ // Item fallbacks
+ 'unknown-item': 'Onbekend Artikel',
+ 'no-description': 'Geen beschrijving beschikbaar',
+
+ // Management
+ 'inventory-management': 'Voorraadbeheer',
+ 'reservation-management': 'Reserveringsbeheer'
+ }
+};
+
+// Translation manager class
+class TranslationManager {
+ constructor() {
+ this.currentLanguage = localStorage.getItem('language') || 'nl'; // Default to Dutch
+ this.init();
+ }
+
+ init() {
+ // Apply saved language preference
+ this.applyTranslations();
+
+ // Set up language toggle buttons if they exist
+ const nlBtn = document.getElementById('nlBtn');
+ const enBtn = document.getElementById('enBtn');
+
+ if (nlBtn && enBtn) {
+ nlBtn.addEventListener('click', () => {
+ this.setLanguage('nl');
+ this.updateLanguageButtons();
+ });
+
+ enBtn.addEventListener('click', () => {
+ this.setLanguage('en');
+ this.updateLanguageButtons();
+ });
+
+ // Set initial button states
+ this.updateLanguageButtons();
+ }
+ }
+
+ updateLanguageButtons() {
+ const nlBtn = document.getElementById('nlBtn');
+ const enBtn = document.getElementById('enBtn');
+
+ if (nlBtn && enBtn) {
+ // Remove active class from both buttons
+ nlBtn.classList.remove('active');
+ enBtn.classList.remove('active');
+
+ // Add active class to current language button
+ if (this.currentLanguage === 'nl') {
+ nlBtn.classList.add('active');
+ } else {
+ enBtn.classList.add('active');
+ }
+ }
+ }
+
+ // Keep the old method for backward compatibility (in case it's used elsewhere)
+ updateLanguageLabels() {
+ this.updateLanguageButtons();
+ }
+
+ setLanguage(lang) {
+ if (lang !== 'en' && lang !== 'nl') {
+ console.warn('Unsupported language:', lang);
+ return;
+ }
+
+ this.currentLanguage = lang;
+ localStorage.setItem('language', lang);
+ this.applyTranslations();
+ this.updateLanguageLabels();
+
+ // Reload dynamic content if function exists
+ if (window.reloadContent) {
+ window.reloadContent();
+ }
+ }
+
+ getLanguage() {
+ return this.currentLanguage;
+ }
+
+ translate(key) {
+ const translation = translations[this.currentLanguage]?.[key];
+ if (!translation) {
+ console.warn(`Translation missing for key: ${key} in language: ${this.currentLanguage}`);
+ // Fallback to Dutch first, then English, then key itself
+ return translations['nl']?.[key] || translations['en']?.[key] || key;
+ }
+ return translation;
+ }
+
+ applyTranslations() {
+ // Update all elements with data-translate attribute
+ document.querySelectorAll('[data-translate]').forEach(element => {
+ const key = element.getAttribute('data-translate');
+ const translation = this.translate(key);
+
+ if (element.tagName === 'INPUT' && element.type === 'submit') {
+ element.value = translation;
+ } else if (element.tagName === 'INPUT' && (element.type === 'text' || element.type === 'password')) {
+ element.placeholder = translation;
+ } else if (element.hasAttribute('title')) {
+ element.title = translation;
+ } else {
+ element.textContent = translation;
+ }
+ });
+
+ // Update page title if it has data-translate
+ const titleElement = document.querySelector('title[data-translate]');
+ if (titleElement) {
+ const key = titleElement.getAttribute('data-translate');
+ document.title = this.translate(key);
+ }
+ }
+
+ // Get translated item data based on current language
+ getLocalizedItem(item) {
+ // Handle the case where item might be null or undefined
+ if (!item) {
+ return {
+ name: this.translate('unknown-item'),
+ description: this.translate('no-description')
+ };
+ }
+
+ let name, description;
+
+ // Handle multilingual name
+ if (typeof item.name === 'object' && item.name !== null) {
+ name = item.name[this.currentLanguage] || item.name.nl || item.name.en || this.translate('unknown-item');
+ } else if (typeof item.name === 'string') {
+ name = item.name;
+ } else {
+ name = this.translate('unknown-item');
+ }
+
+ // Handle multilingual description
+ if (typeof item.description === 'object' && item.description !== null) {
+ description = item.description[this.currentLanguage] || item.description.nl || item.description.en || this.translate('no-description');
+ } else if (typeof item.description === 'string') {
+ description = item.description || this.translate('no-description');
+ } else {
+ description = this.translate('no-description');
+ }
+
+ return {
+ ...item,
+ name: name,
+ description: description
+ };
+ }
+
+ // Get translated status text
+ getStatusText(status) {
+ const statusMap = {
+ 'PENDING': 'pending',
+ 'APPROVED': 'approved',
+ 'REJECTED': 'rejected',
+ 'RETURN_PENDING': 'return-pending',
+ 'RETURNED': 'returned'
+ };
+
+ return this.translate(statusMap[status] || status.toLowerCase());
+ }
+}
+
+// Initialize translation manager when DOM is loaded
+let translationManager;
+document.addEventListener('DOMContentLoaded', () => {
+ translationManager = new TranslationManager();
+ window.translationManager = translationManager;
+});
+
+// Export for use in other files
+window.TranslationManager = TranslationManager;
+
+// Function to ensure translation manager is available
+window.getTranslationManager = function() {
+ if (!window.translationManager) {
+ window.translationManager = new TranslationManager();
+ }
+ return window.translationManager;
+};
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/register.html b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/register.html
new file mode 100644
index 0000000..ade32a9
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/register.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+ Student Registratie - School Magazijn Systeem
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/student-reservations.html b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/student-reservations.html
new file mode 100644
index 0000000..3d129a6
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/student-reservations.html
@@ -0,0 +1,113 @@
+
+
+
+
+
+ Mijn Reserveringen - Student
+
+
+
+
+
+
+
+
+
+
Mijn Reserveringen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Artikelnaam |
+ Hoeveelheid |
+ Locatie |
+ Reserveringsdatum |
+ Status |
+ Acties |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/student.html b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/student.html
new file mode 100644
index 0000000..f32823b
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/public/student.html
@@ -0,0 +1,150 @@
+
+
+
+
+
+ Magazijn Dashboard - Student
+
+
+
+
+
+
+
+
+
+
+
+
Beschikbare Artikelen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Afbeelding |
+ Artikelnaam |
+ Beschrijving |
+ Locatie |
+ Beschikbare Hoeveelheid |
+ Actie |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![Item Image]()
+
+
+
+
+
+
Location:
+
Available Quantity:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/routes/auth.js b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/routes/auth.js
new file mode 100644
index 0000000..903a72a
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/routes/auth.js
@@ -0,0 +1,86 @@
+const express = require('express');
+const bcrypt = require('bcryptjs');
+const jwt = require('jsonwebtoken');
+const User = require('../models/User');
+const router = express.Router();
+
+// Register new student
+router.post('/register', async (req, res) => {
+ try {
+ const { username, password, email } = req.body;
+
+ // Check if email already exists
+ const existingEmail = await User.findOne({ email });
+ if (existingEmail) {
+ return res.status(400).json({ message: 'Email already registered' });
+ }
+
+ // Check if username already exists
+ const existingUsername = await User.findOne({ username });
+ if (existingUsername) {
+ return res.status(400).json({ message: 'Username already taken' });
+ }
+
+ // Hash password
+ const hashedPassword = await bcrypt.hash(password, 10);
+
+ // Create new student user
+ const user = new User({
+ username,
+ password: hashedPassword,
+ email,
+ role: 'student' // Force role to be student
+ });
+
+ await user.save();
+
+ // Create and send token
+ const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '24h' });
+
+ res.status(201).json({
+ message: 'Student account created successfully',
+ token,
+ role: user.role,
+ username: user.username
+ });
+ } catch (error) {
+ if (error.name === 'ValidationError') {
+ return res.status(400).json({
+ message: 'Invalid email format. Email must be in the format: 123456@vistacollege.nl'
+ });
+ }
+ res.status(500).json({ message: 'Server error' });
+ }
+});
+
+// Login
+router.post('/login', async (req, res) => {
+ try {
+ const { username, password } = req.body;
+
+ const user = await User.findOne({ username: username.toLowerCase() });
+
+ if (!user) {
+ return res.status(401).json({ message: 'Invalid credentials' });
+ }
+
+ const passwordMatch = await bcrypt.compare(password, user.password);
+
+ if (!passwordMatch) {
+ return res.status(401).json({ message: 'Invalid credentials' });
+ }
+
+ const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '24h' });
+
+ res.json({
+ token,
+ role: user.role,
+ username: user.username
+ });
+ } catch (error) {
+ console.error('Login error:', error);
+ res.status(500).json({ message: 'Server error' });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/routes/items.js b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/routes/items.js
new file mode 100644
index 0000000..af6594a
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/routes/items.js
@@ -0,0 +1,153 @@
+const express = require('express');
+const multer = require('multer');
+const path = require('path');
+const { auth, adminOnly } = require('../middleware/auth');
+const Item = require('../models/Item');
+const router = express.Router();
+
+// Configure multer for image upload
+const storage = multer.diskStorage({
+ destination: (req, file, cb) => {
+ cb(null, 'public/images/items/');
+ },
+ filename: (req, file, cb) => {
+ const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
+ cb(null, 'item-' + uniqueSuffix + path.extname(file.originalname));
+ }
+});
+
+const upload = multer({
+ storage: storage,
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Not an image! Please upload an image.'), false);
+ }
+ }
+});
+
+// Get all items
+router.get('/', auth, async (req, res) => {
+ try {
+ const items = await Item.find();
+ res.json(items);
+ } catch (error) {
+ res.status(500).json({ message: 'Server error' });
+ }
+});
+
+// Get single item by ID
+router.get('/:id', auth, async (req, res) => {
+ try {
+ const item = await Item.findById(req.params.id);
+ if (!item) {
+ return res.status(404).json({ message: 'Item not found' });
+ }
+ res.json(item);
+ } catch (error) {
+ res.status(500).json({ message: 'Server error' });
+ }
+});
+
+// Add new item (admin only)
+router.post('/', auth, adminOnly, upload.single('image'), async (req, res) => {
+ try {
+ let name, description;
+
+ // Parse multilingual name data
+ try {
+ name = typeof req.body.name === 'string' ? JSON.parse(req.body.name) : req.body.name;
+ } catch (e) {
+ // If parsing fails, assume it's a simple string and create multilingual object
+ name = { nl: req.body.name, en: req.body.name };
+ }
+
+ // Parse multilingual description data
+ try {
+ description = typeof req.body.description === 'string' ? JSON.parse(req.body.description) : req.body.description;
+ } catch (e) {
+ // If parsing fails, assume it's a simple string and create multilingual object
+ description = { nl: req.body.description || '', en: req.body.description || '' };
+ }
+
+ const itemData = {
+ name: name,
+ description: description,
+ location: req.body.location,
+ quantity: parseInt(req.body.quantity)
+ };
+
+ // If an image was uploaded, set the imageUrl
+ if (req.file) {
+ itemData.imageUrl = `/images/items/${req.file.filename}`;
+ }
+
+ const item = new Item(itemData);
+ await item.save();
+ res.status(201).json(item);
+ } catch (error) {
+ res.status(400).json({ message: error.message });
+ }
+});
+
+// Update item (admin only)
+router.put('/:id', auth, adminOnly, upload.single('image'), async (req, res) => {
+ try {
+ let name, description;
+
+ // Parse multilingual name data
+ try {
+ name = typeof req.body.name === 'string' ? JSON.parse(req.body.name) : req.body.name;
+ } catch (e) {
+ // If parsing fails, assume it's a simple string and create multilingual object
+ name = { nl: req.body.name, en: req.body.name };
+ }
+
+ // Parse multilingual description data
+ try {
+ description = typeof req.body.description === 'string' ? JSON.parse(req.body.description) : req.body.description;
+ } catch (e) {
+ // If parsing fails, assume it's a simple string and create multilingual object
+ description = { nl: req.body.description || '', en: req.body.description || '' };
+ }
+
+ const updateData = {
+ name: name,
+ description: description,
+ location: req.body.location,
+ quantity: parseInt(req.body.quantity)
+ };
+
+ // If an image was uploaded, set the new imageUrl
+ if (req.file) {
+ updateData.imageUrl = `/images/items/${req.file.filename}`;
+ } else if (req.body.imageUrl !== undefined) {
+ // If imageUrl is explicitly provided (including empty string for removal)
+ updateData.imageUrl = req.body.imageUrl || '/images/default-item.png';
+ }
+
+ const item = await Item.findByIdAndUpdate(req.params.id, updateData, { new: true });
+ if (!item) {
+ return res.status(404).json({ message: 'Item not found' });
+ }
+ res.json(item);
+ } catch (error) {
+ res.status(400).json({ message: error.message });
+ }
+});
+
+// Delete item (admin only)
+router.delete('/:id', auth, adminOnly, async (req, res) => {
+ try {
+ const item = await Item.findByIdAndDelete(req.params.id);
+ if (!item) {
+ return res.status(404).json({ message: 'Item not found' });
+ }
+ res.json({ message: 'Item deleted successfully' });
+ } catch (error) {
+ res.status(500).json({ message: error.message });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/routes/reservations.js b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/routes/reservations.js
new file mode 100644
index 0000000..f2bf75e
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/routes/reservations.js
@@ -0,0 +1,219 @@
+const express = require('express');
+const { auth, adminOnly } = require('../middleware/auth');
+const Reservation = require('../models/Reservation');
+const Item = require('../models/Item');
+const router = express.Router();
+
+// Get all reservations (admin only)
+router.get('/', auth, adminOnly, async (req, res) => {
+ try {
+ const reservations = await Reservation.find({ status: { $ne: 'ARCHIVED' } })
+ .populate('userId', 'username')
+ .populate('itemId')
+ .exec();
+
+ if (!reservations) {
+ return res.json([]); // Return empty array if no reservations
+ }
+
+ const formattedReservations = reservations
+ .filter(reservation => reservation.userId && reservation.itemId) // Filter out any invalid references
+ .map(reservation => ({
+ _id: reservation._id,
+ studentName: reservation.userId.username,
+ itemName: reservation.itemId.name,
+ location: reservation.itemId.location,
+ status: reservation.status,
+ quantity: reservation.quantity || 1,
+ reservedDate: reservation.reservedDate
+ }));
+
+ res.json(formattedReservations);
+ } catch (error) {
+ console.error('Error fetching reservations:', error);
+ res.status(500).json({
+ message: 'Failed to load reservations',
+ error: error.message
+ });
+ }
+});
+
+// Get user's reservations
+router.get('/my', auth, async (req, res) => {
+ try {
+ const reservations = await Reservation.find({
+ userId: req.user._id,
+ status: { $ne: 'ARCHIVED' }
+ }).populate('itemId');
+
+ // Filter out reservations with deleted items and format the response
+ const formattedReservations = reservations
+ .filter(reservation => reservation.itemId) // Only include reservations with valid items
+ .map(reservation => ({
+ _id: reservation._id,
+ itemName: reservation.itemId.name,
+ location: reservation.itemId.location,
+ status: reservation.status,
+ quantity: reservation.quantity || 1,
+ reservedDate: reservation.reservedDate
+ }));
+
+ res.json(formattedReservations);
+ } catch (error) {
+ console.error('Error fetching user reservations:', error);
+ res.status(500).json({
+ message: 'Failed to load reservations',
+ error: error.message
+ });
+ }
+});
+
+// Create reservation
+router.post('/', auth, async (req, res) => {
+ try {
+ const item = await Item.findById(req.body.itemId);
+ if (!item) {
+ return res.status(404).json({ message: 'Item not found' });
+ }
+
+ const requestedQuantity = parseInt(req.body.quantity) || 1;
+ if (item.quantity < item.reserved + requestedQuantity) {
+ return res.status(400).json({ message: 'Requested quantity is not available' });
+ }
+
+ const reservation = new Reservation({
+ itemId: req.body.itemId,
+ userId: req.user._id,
+ quantity: requestedQuantity
+ });
+
+ await reservation.save();
+ item.reserved += requestedQuantity;
+ await item.save();
+
+ res.status(201).json(reservation);
+ } catch (error) {
+ res.status(400).json({ message: error.message });
+ }
+});
+
+// Update reservation status (admin can update any, students can only request returns)
+router.patch('/:id', auth, async (req, res) => {
+ try {
+ const reservation = await Reservation.findById(req.params.id).populate('userId');
+ if (!reservation) {
+ return res.status(404).json({ message: 'Reservation not found' });
+ }
+
+ // Check authorization
+ const isAdmin = req.user.role === 'admin';
+ const isOwner = reservation.userId._id.toString() === req.user._id.toString();
+ const isRequestingReturn = req.body.status === 'RETURN_PENDING';
+
+ // Students can only request returns on their own approved reservations
+ if (!isAdmin) {
+ if (!isOwner) {
+ return res.status(403).json({
+ message: 'Not authorized. Students can only request return of their own items.'
+ });
+ }
+ if (!isRequestingReturn) {
+ return res.status(403).json({
+ message: 'Students can only request returns, not change other statuses.'
+ });
+ }
+ if (reservation.status !== 'APPROVED') {
+ return res.status(400).json({
+ message: 'Can only request return for approved items'
+ });
+ }
+ }
+
+ const item = await Item.findById(reservation.itemId);
+ if (!item) {
+ return res.status(404).json({ message: 'Item not found' });
+ }
+
+ const oldStatus = reservation.status;
+ const newStatus = req.body.status;
+ reservation.status = newStatus;
+
+ // Include quantity in the update if provided
+ if (req.body.quantity !== undefined) {
+ reservation.quantity = req.body.quantity;
+ }
+
+ await reservation.save();
+
+ // Update item reserved count based on status change
+ if (oldStatus === 'PENDING' && newStatus === 'REJECTED') {
+ item.reserved = Math.max(0, item.reserved - (reservation.quantity || 1));
+ await item.save();
+ } else if (oldStatus === 'RETURN_PENDING' && newStatus === 'RETURNED') {
+ // Admin approved the return request
+ item.reserved = Math.max(0, item.reserved - (reservation.quantity || 1));
+ await item.save();
+ } else if (oldStatus === 'APPROVED' && newStatus === 'RETURNED') {
+ // Admin directly marked approved item as returned
+ item.reserved = Math.max(0, item.reserved - (reservation.quantity || 1));
+ await item.save();
+ }
+
+ res.json(reservation);
+ } catch (error) {
+ console.error('Error updating reservation:', error);
+ res.status(400).json({ message: error.message });
+ }
+});
+
+// Delete reservation (admin can delete any, students can only delete their own)
+router.delete('/:id', auth, async (req, res) => {
+ try {
+ const reservation = await Reservation.findById(req.params.id).populate('userId');
+ if (!reservation) {
+ return res.status(404).json({ message: 'Reservation not found' });
+ }
+
+ // Check if user is authorized to delete this reservation
+ if (req.user.role !== 'admin' && reservation.userId._id.toString() !== req.user._id.toString()) {
+ return res.status(403).json({ message: 'Not authorized to delete this reservation' });
+ }
+
+ // Update item's reserved count
+ const item = await Item.findById(reservation.itemId);
+ if (item) {
+ item.reserved = Math.max(0, item.reserved - 1); // Ensure it doesn't go below 0
+ await item.save();
+ }
+
+ await Reservation.findByIdAndDelete(req.params.id);
+ res.json({ message: 'Reservation deleted successfully' });
+ } catch (error) {
+ res.status(500).json({ message: error.message });
+ }
+});
+
+// Archive reservation (admin only)
+router.patch('/:id/archive', auth, adminOnly, async (req, res) => {
+ try {
+ const reservation = await Reservation.findById(req.params.id);
+ if (!reservation) {
+ return res.status(404).json({ message: 'Reservation not found' });
+ }
+
+ // Only allow archiving of returned reservations
+ if (reservation.status !== 'RETURNED') {
+ return res.status(400).json({ message: 'Can only archive returned reservations' });
+ }
+
+ reservation.status = 'ARCHIVED';
+ await reservation.save();
+
+ res.json({ message: 'Reservation archived successfully' });
+ } catch (error) {
+ console.error('Error archiving reservation:', error);
+ res.status(500).json({ message: error.message });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/routes/upload.js b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/routes/upload.js
new file mode 100644
index 0000000..e283e8e
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/routes/upload.js
@@ -0,0 +1,43 @@
+const express = require('express');
+const multer = require('multer');
+const path = require('path');
+const { auth, adminOnly } = require('../middleware/auth');
+const router = express.Router();
+
+// Configure multer for image upload
+const storage = multer.diskStorage({
+ destination: (req, file, cb) => {
+ cb(null, 'public/images/items/');
+ },
+ filename: (req, file, cb) => {
+ const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
+ cb(null, 'item-' + uniqueSuffix + path.extname(file.originalname));
+ }
+});
+
+const upload = multer({
+ storage: storage,
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Not an image! Please upload an image.'), false);
+ }
+ }
+});
+
+// Upload image route
+router.post('/', auth, adminOnly, upload.single('image'), async (req, res) => {
+ try {
+ if (!req.file) {
+ return res.status(400).json({ message: 'No file uploaded' });
+ }
+
+ const imageUrl = `/images/items/${req.file.filename}`;
+ res.json({ imageUrl });
+ } catch (error) {
+ res.status(500).json({ message: error.message });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/seed.js b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/seed.js
new file mode 100644
index 0000000..de9c507
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/seed.js
@@ -0,0 +1,109 @@
+const bcrypt = require('bcryptjs');
+const mongoose = require('mongoose');
+require('dotenv').config();
+
+const User = require('./models/User');
+const Item = require('./models/Item');
+
+async function seedDatabase() {
+ try {
+ // Connect to MongoDB
+ await mongoose.connect(process.env.MONGODB_URI);
+ console.log('Connected to MongoDB');
+
+ // Clear existing data
+ await User.deleteMany({});
+ await Item.deleteMany({});
+
+ // Create admin user
+ const adminPassword = await bcrypt.hash('admin123', 10);
+ await User.create({
+ username: 'admin',
+ password: adminPassword,
+ role: 'admin'
+ });
+
+ // Create test student
+ const studentPassword = await bcrypt.hash('student123', 10);
+ await User.create({
+ username: 'student',
+ password: studentPassword,
+ email: '123456@vistacollege.nl',
+ role: 'student'
+ });
+
+ // Create some test items with multilingual support
+ const items = [
+ {
+ name: {
+ en: 'Laptop',
+ nl: 'Laptop'
+ },
+ description: {
+ en: 'High-performance laptop for programming and design work',
+ nl: 'Krachtige laptop voor programmeren en ontwerpwerk'
+ },
+ location: 'Heerlen',
+ quantity: 5
+ },
+ {
+ name: {
+ en: 'Projector',
+ nl: 'Beamer'
+ },
+ description: {
+ en: 'HD projector for presentations and lectures',
+ nl: 'HD-beamer voor presentaties en lezingen'
+ },
+ location: 'Maastricht',
+ quantity: 3
+ },
+ {
+ name: {
+ en: 'Microscope',
+ nl: 'Microscoop'
+ },
+ description: {
+ en: 'Digital microscope for laboratory work',
+ nl: 'Digitale microscoop voor laboratoriumwerk'
+ },
+ location: 'Sittard',
+ quantity: 4
+ },
+ {
+ name: {
+ en: 'Tablet',
+ nl: 'Tablet'
+ },
+ description: {
+ en: 'Portable tablet for mobile learning and presentations',
+ nl: 'Draagbare tablet voor mobiel leren en presentaties'
+ },
+ location: 'Heerlen',
+ quantity: 10
+ },
+ {
+ name: {
+ en: 'Camera',
+ nl: 'Camera'
+ },
+ description: {
+ en: 'Professional DSLR camera for photography courses',
+ nl: 'Professionele spiegelreflexcamera voor fotografiecursussen'
+ },
+ location: 'Maastricht',
+ quantity: 2
+ }
+ ];
+
+ await Item.insertMany(items);
+
+ console.log('Database seeded successfully');
+ process.exit(0);
+ } catch (error) {
+ console.error('Error seeding database:', error);
+ process.exit(1);
+ }
+}
+
+seedDatabase();
\ No newline at end of file
diff --git a/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/server.js b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/server.js
new file mode 100644
index 0000000..c325b1a
--- /dev/null
+++ b/projects/year_02/challenge_15/Challenge_15_Magazijn_App_Maken/server.js
@@ -0,0 +1,86 @@
+const express = require('express');
+const mongoose = require('mongoose');
+const cors = require('cors');
+const path = require('path');
+const os = require('os');
+require('dotenv').config();
+
+const app = express();
+
+// Load models first
+const User = require('./models/User');
+const Item = require('./models/Item');
+const Reservation = require('./models/Reservation');
+
+// Middleware
+app.use(cors());
+app.use(express.json());
+app.use(express.static('public'));
+
+// Connect to MongoDB
+mongoose.connect(process.env.MONGODB_URI)
+ .then(async () => {
+ console.log('Database connected and models initialized successfully');
+ })
+ .catch(err => {
+ console.error('MongoDB connection error:', err);
+ // Don't exit, just log the error
+ });
+
+// Routes
+app.use('/api/auth', require('./routes/auth'));
+app.use('/api/items', require('./routes/items'));
+app.use('/api/reservations', require('./routes/reservations'));
+app.use('/api/upload', require('./routes/upload'));
+
+// Error handling middleware
+app.use((err, req, res, next) => {
+ console.error('Error:', err);
+ res.status(500).json({
+ message: 'Internal server error',
+ error: process.env.NODE_ENV === 'development' ? err.message : undefined
+ });
+});
+
+// Serve static files
+app.get('*', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'index.html'));
+});
+
+const PORT = process.env.PORT || 3000;
+
+// Get local IP addresses
+function getLocalIPs() {
+ const interfaces = os.networkInterfaces();
+ const addresses = [];
+
+ for (const interfaceName in interfaces) {
+ const interface = interfaces[interfaceName];
+ for (const address of interface) {
+ // Skip internal and non-IPv4 addresses
+ if (address.family === 'IPv4' && !address.internal) {
+ addresses.push(address.address);
+ }
+ }
+ }
+ return addresses;
+}
+
+// Handle uncaught exceptions
+process.on('uncaughtException', (err) => {
+ console.error('Uncaught Exception:', err);
+});
+
+// Handle unhandled promise rejections
+process.on('unhandledRejection', (reason, promise) => {
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason);
+});
+
+app.listen(PORT, () => {
+ const localIPs = getLocalIPs();
+ console.log(`Server is running on:`);
+ console.log(`- Local: http://localhost:${PORT}`);
+ localIPs.forEach(ip => {
+ console.log(`- Network: http://${ip}:${PORT}`);
+ });
+});
\ No newline at end of file