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) => (
-
- );
-
- const videoBlock = (video) => {
- if (video?.video_files?.length > 0) {
- return (
-
-
-
- Your browser does not support the video tag.
-
-
-
)
- }
+ 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 */}
+
+
+
+
+ 🥐
+
+ Croissant Delight
+
-
-
-
+
+
+
+
+
-
-
+ {/* 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}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
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}
+
+ ))}
+
+
+
+
+ {/* Footer */}
+
+
+
+
+
+ 🥐
+ Croissant Delight
+
+
+ Menghadirkan kelezatan roti artisan Perancis ke rumah Anda dengan bahan terbaik dan proses yang higienis.
+
+
+
+
+
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) })}
+ />
+
+
+ setOrderData({ ...orderData, metode_pembayaran: e.target.value })}
+ >
+ Cash (Tunai)
+ E-Wallet (QRIS)
+
+
+
+
+ {orderData.metode_pembayaran === 'qris' && (
+
+
Scan QRIS Croissant Delight
+
+
+
+
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