diff --git a/backend/src/db/api/orders.js b/backend/src/db/api/orders.js index 279459c..d4b205b 100644 --- a/backend/src/db/api/orders.js +++ b/backend/src/db/api/orders.js @@ -1,4 +1,3 @@ - const db = require('../models'); const FileDBApi = require('./file'); const crypto = require('crypto'); @@ -61,9 +60,25 @@ module.exports = class OrdersDBApi { - await orders.setItems(data.items || [], { - transaction, - }); + if (data.items && Array.isArray(data.items)) { + await orders.setItems(data.items, { + transaction, + }); + } + + // Custom logic to handle order items with quantity and price + if (data.orderItems && Array.isArray(data.orderItems)) { + for (const item of data.orderItems) { + await db.order_items.create({ + orderId: orders.id, + menuId: item.menuId, + quantity: item.quantity, + harga: item.harga, + createdById: currentUser.id, + updatedById: currentUser.id, + }, { transaction }); + } + } @@ -508,5 +523,4 @@ module.exports = class OrdersDBApi { } -}; - +}; \ No newline at end of file diff --git a/backend/src/db/migrations/20260206120000-grant-public-permissions.js b/backend/src/db/migrations/20260206120000-grant-public-permissions.js new file mode 100644 index 0000000..b7fb9ee --- /dev/null +++ b/backend/src/db/migrations/20260206120000-grant-public-permissions.js @@ -0,0 +1,40 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + const [roles] = await queryInterface.sequelize.query( + `SELECT id FROM roles WHERE name = 'Public' LIMIT 1;` + ); + const publicRoleId = roles[0]?.id; + + if (!publicRoleId) { + console.log('Public role not found, skipping migration'); + return; + } + + const [permissions] = await queryInterface.sequelize.query( + `SELECT id, name FROM permissions WHERE name IN ('READ_MENUS', 'CREATE_ORDERS', 'CREATE_ORDER_ITEMS');` + ); + + const rolesPermissions = permissions.map(p => ({ + "createdAt": new Date(), + "updatedAt": new Date(), + "roles_permissionsId": publicRoleId, + "permissionId": p.id + })); + + if (rolesPermissions.length > 0) { + for (const rp of rolesPermissions) { + const [existing] = await queryInterface.sequelize.query( + `SELECT * FROM "rolesPermissionsPermissions" WHERE "roles_permissionsId" = '${rp.roles_permissionsId}' AND "permissionId" = '${rp.permissionId}';` + ); + if (existing.length === 0) { + await queryInterface.bulkInsert('rolesPermissionsPermissions', [rp]); + } + } + } + }, + + down: async (queryInterface, Sequelize) => { + } +}; \ No newline at end of file diff --git a/backend/src/index.js b/backend/src/index.js index e124cb5..1ea8ef8 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -97,9 +97,9 @@ app.use('/api/roles', passport.authenticate('jwt', {session: false}), rolesRoute app.use('/api/permissions', passport.authenticate('jwt', {session: false}), permissionsRoutes); -app.use('/api/menus', passport.authenticate('jwt', {session: false}), menusRoutes); +app.use('/api/menus', menusRoutes); -app.use('/api/orders', passport.authenticate('jwt', {session: false}), ordersRoutes); +app.use('/api/orders', ordersRoutes); app.use('/api/order_items', passport.authenticate('jwt', {session: false}), order_itemsRoutes); diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index 55559d2..76ca165 100644 --- a/frontend/src/components/NavBarItem.tsx +++ b/frontend/src/components/NavBarItem.tsx @@ -1,6 +1,5 @@ -import React, {useEffect, useRef} from 'react' +import React, { useEffect, useRef, useState } from 'react' import Link from 'next/link' -import { useState } from 'react' import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import BaseDivider from './BaseDivider' import BaseIcon from './BaseIcon' @@ -129,4 +128,4 @@ export default function NavBarItem({ item }: Props) { } return
{NavBarItemComponentContents}
-} +} \ No newline at end of file diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 1b9907d..26c3572 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -1,5 +1,4 @@ -import React, { ReactNode, useEffect } from 'react' -import { useState } from 'react' +import React, { ReactNode, useEffect, useState } from 'react' import jwt from 'jsonwebtoken'; import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' import menuAside from '../menuAside' @@ -126,4 +125,4 @@ export default function LayoutAuthenticated({ ) -} +} \ No newline at end of file diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index bf72cca..a110c42 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,166 +1,427 @@ - import React, { useEffect, useState } from 'react'; import type { ReactElement } from 'react'; import Head from 'next/head'; -import Link from 'next/link'; -import BaseButton from '../components/BaseButton'; -import CardBox from '../components/CardBox'; -import SectionFullScreen from '../components/SectionFullScreen'; +import axios from 'axios'; +import { mdiCart, mdiSilverwareForkKnife, mdiClockOutline, mdiCheckDecagram, mdiArrowRight } from '@mdi/js'; import LayoutGuest from '../layouts/Guest'; -import BaseDivider from '../components/BaseDivider'; -import BaseButtons from '../components/BaseButtons'; import { getPageTitle } from '../config'; -import { useAppSelector } from '../stores/hooks'; -import CardBoxComponentTitle from "../components/CardBoxComponentTitle"; -import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'; +import SectionMain from '../components/SectionMain'; +import CardBox from '../components/CardBox'; +import BaseButton from '../components/BaseButton'; +import BaseButtons from '../components/BaseButtons'; +import BaseIcon from '../components/BaseIcon'; +import ImageField from '../components/ImageField'; +import LoadingSpinner from '../components/LoadingSpinner'; +import CardBoxModal from '../components/CardBoxModal'; +import FormField from '../components/FormField'; +export default function LandingPage() { + const [menus, setMenus] = useState([]); + const [loading, setLoading] = useState(true); + const [isOrderModalOpen, setIsOrderModalOpen] = useState(false); + const [selectedProduct, setSelectedProduct] = useState(null); + const [orderData, setOrderData] = useState({ + nama_pemesan: '', + quantity: 1, + metode_pembayaran: 'tunai' + }); + const [isSubmitting, setIsSubmitting] = useState(false); + const [orderSuccess, setOrderSuccess] = useState(false); -export default function Starter() { - const [illustrationImage, setIllustrationImage] = useState({ - src: undefined, - photographer: undefined, - photographer_url: undefined, - }) - const [illustrationVideo, setIllustrationVideo] = useState({video_files: []}) - const [contentType, setContentType] = useState('image'); - const [contentPosition, setContentPosition] = useState('left'); - const textColor = useAppSelector((state) => state.style.linkColor); - - const title = 'Croissant Order Laravel' - - // Fetch Pexels image/video - useEffect(() => { - async function fetchData() { - const image = await getPexelsImage(); - const video = await getPexelsVideo(); - setIllustrationImage(image); - setIllustrationVideo(video); - } - fetchData(); - }, []); - - const imageBlock = (image) => ( -
-
- - Photo by {image?.photographer} on Pexels - -
-
- ); - - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- -
- - Video by {video.user.name} on Pexels - -
-
) - } + useEffect(() => { + const fetchMenus = async () => { + try { + const response = await axios.get('/menus'); + setMenus(response.data.rows || []); + } catch (error) { + console.error('Failed to fetch menus:', error); + } finally { + setLoading(false); + } }; + fetchMenus(); + }, []); + + const handleOrderClick = (product: any) => { + setSelectedProduct(product); + setIsOrderModalOpen(true); + setOrderSuccess(false); + }; + + const handleOrderSubmit = async () => { + if (!orderData.nama_pemesan) { + alert('Mohon isi nama pemesan'); + return; + } + + setIsSubmitting(true); + try { + const total_harga = (selectedProduct.harga || 0) * orderData.quantity; + + const payload = { + nama_pemesan: orderData.nama_pemesan, + total_harga: total_harga, + metode_pembayaran: orderData.metode_pembayaran, + status_pesanan: 'belum_dibuat', + status_pembayaran: 'belum_dibayar', + ordered_at: new Date().toISOString(), + orderItems: [ + { + menuId: selectedProduct.id, + quantity: orderData.quantity, + harga: selectedProduct.harga + } + ] + }; + + await axios.post('/orders', { data: payload }); + + setOrderSuccess(true); + setTimeout(() => { + setIsOrderModalOpen(false); + setOrderData({ nama_pemesan: '', quantity: 1, metode_pembayaran: 'tunai' }); + }, 3000); + } catch (error) { + console.error('Failed to submit order:', error); + alert('Gagal mengirim pesanan. Silakan coba lagi.'); + } finally { + setIsSubmitting(false); + } + }; return ( -
+
- {getPageTitle('Starter Page')} + {getPageTitle('Croissant Delight - Artisanal Bakery')} - -
- {contentType === 'image' && contentPosition !== 'background' - ? imageBlock(illustrationImage) - : null} - {contentType === 'video' && contentPosition !== 'background' - ? videoBlock(illustrationVideo) - : null} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

+ {/* Header/Navigation */} + - - + {/* Hero Section */} +
+
+
+ +
+
+
+ Freshly baked every morning! ✨ +
+

+ Croissant Hangat,
+ Fresh Setiap Hari 🥐 +

+

+ Dibuat dengan bahan premium, butter asli Perancis, dan dipanggang segar setiap pagi untuk menghadirkan kebahagiaan di setiap gigitan. +

+
+ document.getElementById('menu-section')?.scrollIntoView({ behavior: 'smooth' })} + className="px-10 py-4 text-lg rounded-2xl shadow-lg shadow-orange-200 hover:shadow-orange-300 transition-all active:scale-95" + icon={mdiArrowRight} + /> + +
+
+
+
+
+ Fresh Croissants +
+
+ +
+
+

100% Halal

+

Bahan Premium

+
+
+
+
- -
-

© 2026 {title}. All rights reserved

- - Privacy Policy - + + {/* Features - Minimalist style */} +
+
+
+ {[ + { icon: mdiSilverwareForkKnife, title: 'Artisanal Quality', desc: 'Setiap adonan dibuat dengan teknik tradisional untuk tekstur yang sempurna.' }, + { icon: mdiClockOutline, title: 'Freshly Baked', desc: 'Kami tidak pernah menyajikan produk sisa kemarin. Selalu baru setiap jam 7 pagi.' }, + { icon: mdiCart, title: 'Easy Delivery', desc: 'Pesan melalui website dan kami akan kirimkan ke tempat Anda dengan aman.' } + ].map((feature, idx) => ( +
+
+ +
+

{feature.title}

+

{feature.desc}

+
+ ))} +
+
+ +
+

Menu Unggulan Kami

+

Dari varian klasik hingga kreasi modern, temukan rasa yang paling menggugah seleramu hari ini.

+
+ + {loading ? ( +
+ +

Menyiapkan menu segar...

+
+ ) : ( +
+ {menus.map((menu: any) => ( +
+
+ + {menu.status === 'habis' && ( +
+ Habis +
+ )} +
+ + Best Seller + +
+
+
+

{menu.nama_menu}

+

{menu.deskripsi}

+
+
+
+
+ Harga + + Rp {Number(menu.harga).toLocaleString('id-ID')} + +
+ handleOrderClick(menu)} + className="rounded-2xl px-8 py-3 shadow-md shadow-orange-100 active:scale-95 transition-all" + icon={mdiCart} + /> +
+
+
+ ))} +
+ )} +
+ + {/* Footer */} +
+
+
+
+
+ 🥐 + Croissant Delight +
+

+ Menghadirkan kelezatan roti artisan Perancis ke rumah Anda dengan bahan terbaik dan proses yang higienis. +

+
+
+

Quick Links

+ +
+
+

Follow Us

+
+
+ 📸 +
+
+ 🐦 +
+
+ 📘 +
+
+
+
+
+

© 2026 Croissant Delight. Crafted with ❤️ for Croissant Lovers.

+ +
+
+
+ + {/* Order Modal */} + setIsOrderModalOpen(false)} + isLoading={isSubmitting} + hasFooter={!orderSuccess} + > + {orderSuccess ? ( +
+
+ +
+

Pesanan Terkirim!

+

+ Terima kasih {orderData.nama_pemesan}.
+ Pesanan Anda sedang diproses dan akan segera diantar. +

+ setIsOrderModalOpen(false)} + className="rounded-xl px-12 py-3 border border-gray-200" + /> +
+ ) : ( +
+
+
+ +
+
+

{selectedProduct?.nama_menu}

+

Rp {Number(selectedProduct?.harga).toLocaleString('id-ID')} / pcs

+
+
+ + + setOrderData({ ...orderData, nama_pemesan: e.target.value })} + placeholder="Masukkan nama Anda" + required + /> + + +
+ + setOrderData({ ...orderData, quantity: Math.max(1, parseInt(e.target.value) || 1) })} + /> + + + + +
+ + {orderData.metode_pembayaran === 'qris' && ( +
+

Scan QRIS Croissant Delight

+
+ QRIS +
+

Lakukan pembayaran sekarang dan simpan bukti bayarnya.

+
+ )} + +
+
+

Total Pembayaran

+

+ Rp {((selectedProduct?.harga || 0) * orderData.quantity).toLocaleString('id-ID')} +

+
+
+
+ )} +
+ +
); } -Starter.getLayout = function getLayout(page: ReactElement) { +LandingPage.getLayout = function getLayout(page: ReactElement) { return {page}; -}; - +}; \ No newline at end of file