Compare commits

..

18 Commits

Author SHA1 Message Date
karyamanswasta
2add1c5090 ok 2025-08-27 08:15:48 +07:00
karyamanswasta
3fbe0bb870 ok 2025-08-27 07:41:31 +07:00
karyamanswasta
dae7fb9221 ok 2025-08-27 07:12:39 +07:00
karyamanswasta
6ed982d6ef ok 2025-08-27 07:00:14 +07:00
karyamanswasta
df203447a9 ok 2025-08-27 05:54:02 +07:00
karyamanswasta
f58b40c70d ok 2025-08-27 03:20:49 +07:00
karyamanswasta
6127415e37 ok 2025-08-27 01:55:07 +07:00
karyamanswasta
b28c6ed0fe ok 2025-08-26 13:07:13 +07:00
Vassshhh
67cf759b31 ok 2025-08-25 23:41:35 +07:00
Vassshhh
53e091d3a4 ok 2025-07-28 01:15:07 +07:00
everythingonblack
3a431b1b14 ok 2025-05-23 10:50:39 +07:00
everythingonblack
69a07be3cd ok 2025-05-22 16:43:50 +07:00
everythingonblack
b012517568 ok 2025-05-22 02:15:12 +07:00
everythingonblack
3e35468f2c ok 2025-05-21 16:52:38 +07:00
everythingonblack
df7c4f737c ok 2025-05-20 17:47:43 +07:00
everythingonblack
b726ae6919 ok 2025-05-16 19:54:09 +07:00
everythingonblack
da317f83c9 ok 2025-05-15 17:52:24 +07:00
everythingonblack
f6482d24d2 ok 2025-05-15 15:49:40 +07:00
55 changed files with 12746 additions and 8170 deletions

58
note/detailUI.md Normal file
View File

@@ -0,0 +1,58 @@
**Overlay/Status**
- Loader Overlay: layar penuh dengan spinner `ThreeDots` dan pesan `screenMessag
e`; muncul saat `loading` true.
- Grayscale State: seluruh konten `div.Cafe` menjadi abu-abu saat `isExceededDea
dline` true (kelas `grayscale`).
- Modal Pesan: “Kafe sedang tidak tersedia” dapat muncul via `setModal("message"
, ...)` ketika `isExceededDeadline` dari socket; modalnya dikelola global (di lu
ar file ini).
**Header**
- Bar Atas “Menu”: judul “Menu”, info toko (nama, gambar), profil user, opsi log
out, akses pegawai, table code.
- Edit Mode Toggle: kontrol untuk mengaktifkan/menonaktifkan mode edit kategori/
item (prop `isEditMode` + `setIsEditMode`).
**Branding/Watermark**
- Dev Watermark (atas): `div.Watermark` kecil hanya tampil jika `API_BASE_URL` b
ukan domain resmi prod/dev (indikator environment).
- Footer Watermark: komponen `<Watermark />` di bagian paling bawah halaman.
**Music**
- Music Player: widget pemutar/antrian lagu dengan dukungan Spotify; menampilkan
status login Spotify (`isSpotifyNeedLogin`) dan antrian (`queue`), interaksi vi
a `socket`.
**Kategori (ItemTypeLister)**
- Daftar Kategori: list tipe menu (nama + gambar tipe, visibilitas).
- Filter Kategori: memilih 1 kategori untuk menyaring tampilan item (`filterId`)
.
- Edit Kategori: saat `isEditMode` aktif, bisa memilih tipe yang sedang diedit (
`beingEditedType`) dan mengubah urutan tipe (via kontrol di ItemLister).
**Daftar Item per Kategori (ItemLister x N)**
- Section per Kategori: render berulang untuk setiap tipe yang lolos filter. Men
ampilkan:
- Judul/nama kategori dan gambar (bila ada).
- Daftar item dalam kategori (nama, harga, promo, deskripsi, gambar, visibilit
as).
- Aksi Item:
- Tambah item: tombol/form untuk membuat item (owner/akses yang berwenang).
- Ubah item: edit nama, harga, promo, deskripsi, gambar.
- Reorder Kategori: panah/aksi “naik/turun” pada section untuk memindahkan posis
i kategori (memanggil `moveItemType*`).
- Raw Mode: jika tidak edit dan filter spesifik aktif, `raw` true untuk gaya tam
pilan ringkas.
**Sticky Bar (Keranjang & Transaksi)**
- Tombol Cart (kiri, utama): muncul jika bukan edit mode dan (user login atau ke
ranjang > 0).
- Menampilkan jumlah item (dengan “+” jika ada transaksi terakhir), total harg
a “Rp{totalPrice}” atau teks “Open bill” (jika `lastTransaction.payment_type ==
'paylater'`).
- Posisi: sticky di bawah (offset bottom ~40px), lebar responsif.
- Tombol Transactions (kanan, kecil): hanya muncul jika user login; navigasi ke
riwayat transaksi.
material list, material

11075
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,12 +10,15 @@
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"apexcharts": "^5.3.4",
"caniuse-lite": "^1.0.30001690",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"html-to-image": "^1.11.11",
"html2canvas": "^1.4.1",
"jsqr": "^1.4.0",
"lucide-react": "^0.541.0",
"qr-scanner": "^1.4.2",
"qrcode.react": "^3.1.0",
"react": "^18.3.1",
"react-apexcharts": "^1.7.0",

View File

@@ -28,6 +28,15 @@
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>KedaiMaster</title>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-2SKSCVFB2N"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){ dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'G-2SKSCVFB2N');
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -1,14 +1,21 @@
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Aboreto&family=Rubik+Doodle+Shadow&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@200;300;400;500;600;700;800&ital,wght@0,200..800;1,200..800&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@200;300;400;500;600;700;800&display");
:root {
--brand-primary: #73a585; /* general brand (e. g., music player) */
--brand-sage: #6B8F71; /* sage green for active category */
--brand-sage-50: #F0F6F2; /* very light hover bg */
--brand-sage-100: #E9F3ED; /* light hover bg */
--brand-sage-hover: #7FAE7D; /* hover for filled buttons */
--brand-sage-muted: #CFD8D3; /* disabled button */
}
html,
body {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.App {
/* overflow-x: hidden; */
}
.Cafe {
@@ -29,7 +36,7 @@ body {
height: 100%;
width: 100%;
background-image: url(https://i.ibb.co.com/F4FMw1jz/testuseonly.png);
z-index: 1000;
z-index: 0; /* align with item lister */
filter: opacity(0.04);
pointer-events: none;
}
@@ -56,6 +63,142 @@ body {
color: white;
}
/* removed two-column layout; reverted to single column */
/* Ensure sticky cart bar stays above item overlays */
.StickyCartBar {
z-index: 120 !important; /* above content, below modal */
position: fixed;
left: 0;
right: 0;
bottom: 24px;
background: transparent;
box-shadow: none;
pointer-events: none; /* only children capture clicks */
display: flex;
justify-content: center;
align-items: center;
padding: 0 12px;
}
/* Intro slide/fade when bar first appears */
.StickyCartBar.intro { animation: bar-in 450ms ease-out; }
@keyframes bar-in {
0% { opacity: 0; transform: translateY(12px); }
100% { opacity: 1; transform: translateY(0); }
}
/* Floating cart buttons inside sticky bar */
.StickyCartBar .cartBtn {
pointer-events: auto;
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
padding: 0 14px;
border-radius: 999px;
border: none;
background: rgba(115,165,133,0.96);
color: #fff;
backdrop-filter: blur(3px);
box-shadow: 0 12px 28px rgba(115,165,133,0.35);
width: clamp(200px, 60vw, 420px);
transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.2s ease;
position: relative;
}
.StickyCartBar .cartBtn:hover { box-shadow: 0 14px 32px rgba(115,165,133,0.4); transform: translateY(-1px); }
.StickyCartBar .cartBtn:active { transform: translateY(0); }
.StickyCartBar .cartBtn:focus-visible { outline: 3px solid rgba(115,165,133,0.4); outline-offset: 2px; }
/* Bump + ping animation when items are added */
.StickyCartBar .cartBtn.bump { animation: cart-bump 300ms ease; }
.StickyCartBar .cartBtn.bump::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: 0 0 0 0 rgba(115,165,133,0.35);
animation: cart-ping 600ms ease-out;
}
@keyframes cart-bump {
0% { transform: scale(1); }
10% { transform: scale(0.98); }
30% { transform: scale(1.04); }
50% { transform: scale(1.02); }
100% { transform: scale(1); }
}
@keyframes cart-ping {
0% { box-shadow: 0 0 0 0 rgba(115,165,133,0.35); }
100% { box-shadow: 0 0 0 14px rgba(115,165,133,0); }
}
.StickyCartBar .historyBtn {
pointer-events: auto;
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 44px;
margin-left: 8px;
border-radius: 999px;
border: none;
background: rgba(115,165,133,0.96);
color: #fff;
backdrop-filter: blur(3px);
box-shadow: 0 12px 28px rgba(115,165,133,0.35);
transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.2s ease;
}
.StickyCartBar .historyBtn:hover { box-shadow: 0 14px 32px rgba(115,165,133,0.4); transform: translateY(-1px); }
.StickyCartBar .historyBtn:active { transform: translateY(0); }
.StickyCartBar .historyBtn:focus-visible { outline: 3px solid rgba(115,165,133,0.4); outline-offset: 2px; }
/* Subtle pulse to draw attention on new transaction */
.StickyCartBar .historyBtn.pulse { animation: btn-pulse 900ms ease-out; }
@keyframes btn-pulse {
0% { box-shadow: 0 12px 28px rgba(115,165,133,0.35); }
60% { box-shadow: 0 18px 38px rgba(115,165,133,0.5); }
100% { box-shadow: 0 12px 28px rgba(115,165,133,0.35); }
}
.StickyCartBar .summary {
display: flex;
align-items: center;
gap: 8px;
font-weight: 700;
}
.StickyCartBar .value { white-space: nowrap; font-weight: 800; }
.StickyCartBar .icon { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; margin-left: 8px; }
.StickyCartBar .icon { position: relative; }
.StickyCartBar .icon .badge {
position: absolute;
top: -6px;
right: -6px;
width: 18px;
height: 18px;
border-radius: 50%;
background: #ff6b6b;
color: #fff;
font-size: 11px;
font-weight: 800;
line-height: 18px;
text-align: center;
padding: 0;
box-shadow: 0 2px 6px rgba(255,107,107,0.35);
}
.StickyCartBar .icon .badge.pop { animation: badge-pop 280ms ease-out; }
@keyframes badge-pop {
0% { transform: scale(0.8); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
@media (max-width: 420px) {
.StickyCartBar { bottom: 16px; }
.StickyCartBar .cartBtn { height: 46px; }
.StickyCartBar .historyBtn { height: 46px; width: 48px; }
}
.App-link {
color: #61dafb;
}

View File

@@ -71,6 +71,12 @@ function App() {
const [depth, setDepth] = useState(-1);
const [queue, setQueue] = useState([]);
const [newTransaction, setNewTransaction] = useState({});
const queryParams = new URLSearchParams(location.search);
const tokenParams = queryParams.get("token");
if(tokenParams) localStorage.setItem('auth', tokenParams)
const validTransactionStates = [
'new_transaction',
@@ -109,6 +115,10 @@ function App() {
const handleStorageChange = () => {
calculateTotalsFromLocalStorage();
if (!localStorage.getItem("lastTransaction")) {
setLastTransaction(null);
}
};
window.addEventListener("localStorageUpdated", handleStorageChange);
@@ -135,6 +145,7 @@ function App() {
return;
}
setModal('transaction_confirmed', { transactionId: lastTransaction.transactionId })
const myLastTransaction = await checkIsMyTransaction(lastTransaction.transactionId);
console.log(myLastTransaction)
if (myLastTransaction.isMyTransaction) {
@@ -219,7 +230,7 @@ function App() {
});
} else {
socket.emit("checkUserToken", {
token: getLocalStorage("auth"),
token: getLocalStorage("auth") || tokenParams,
shopId,
});
}
@@ -236,24 +247,23 @@ function App() {
});
socket.on("transaction_confirmed", async (data) => {
console.log("transaction notification: " + data);
console.log(JSON.stringify(data));
setModal("transaction_confirmed", data);
localStorage.setItem('cart', []);
const startTime = Date.now(); // Capture the start time
const timeout = 10000; // 10 seconds timeout in milliseconds
// const startTime = Date.now(); // Capture the start time
// const timeout = 10000; // 10 seconds timeout in milliseconds
calculateTotalsFromLocalStorage();
while (localStorage.getItem("lastTransaction") === null) {
if (Date.now() - startTime > timeout) {
return; // Exit the function and don't proceed further
}
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
}
// while (localStorage.getItem("lastTransaction") === null) {
// if (Date.now() - startTime > timeout) {
// return; // Exit the function and don't proceed further
// }
// await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
// }
// If 'lastTransaction' exists, proceed
const lastTransaction = JSON.parse(localStorage.getItem("lastTransaction"));
@@ -270,17 +280,23 @@ function App() {
setModal("transaction_success", data);
// If 'lastTransaction' exists, proceed
localStorage.removeItem("lastTransaction");
if (lastTransaction != null) {
if (localStorage.getItem("lastTransaction")) {
setLastTransaction(null);
console.log('remove last transaction')
localStorage.removeItem("lastTransaction");
window.dispatchEvent(new Event("localStorageUpdated"));
}
});
socket.on("transaction_end", async (data) => {
console.log("transaction notification");
setModal("transaction_end", data);
// If 'lastTransaction' exists, proceed
if (localStorage.getItem("lastTransaction")) {
setLastTransaction(null);
localStorage.removeItem("lastTransaction");
window.dispatchEvent(new Event("localStorageUpdated"));
}
});
socket.on("payment_claimed", async (data) => {
@@ -289,8 +305,15 @@ function App() {
});
socket.on("transaction_failed", async (data) => {
console.log("transaction notification");
console.log(JSON.stringify(data));
setModal("transaction_failed", data);
// If 'lastTransaction' exists, proceed
if (localStorage.getItem("lastTransaction")) {
setLastTransaction(null);
localStorage.removeItem("lastTransaction");
window.dispatchEvent(new Event("localStorageUpdated"));
}
});
@@ -324,7 +347,7 @@ function App() {
setDeviceType("guestDevice");
} else {
console.log(data)
if(data.data.user.cafeId != null) navigate(`/${data.data.user.cafeIdentityName}`, { replace: true });
if (data.data.user.cafeId != null) navigate(`/${data.data.user.cafeIdentityName}`, { replace: true });
setUser(data.data.user);
if (data.data.latestOpenBillTransaction != null) localStorage.setItem('lastTransaction', JSON.stringify(data.data.latestOpenBillTransaction))
if (
@@ -378,30 +401,32 @@ function App() {
};
}, [socket, shopId]);
async function checkIfStillViewingOtherTransaction() {
async function checkIfStillViewingOtherTransaction(data) {
console.log("transaction notification");
console.log(modalContent);
let response;
response = await getTransactionsFromCafe(shopId, 0, true);
transactionList.current = response;
console.log(response);
// Get current URL's search parameters inside the socket event handler
const searchParams = new URLSearchParams(location.search);
let transaction_info = searchParams.get("transactionId") || ''; // Get transactionId or set it to empty string
console.log(transaction_info); // Log the updated transaction_info
if(response[0].transactionId != transaction_info) transactionList.current = response;
const depthh = transactionList.current.findIndex(
let depthh = transactionList.current.findIndex(
item => item.transactionId.toString() === transaction_info.toString()
);
setDepth(depthh);
if (transaction_info != response[0].transactionId)
setDepth(depthh);
else setModal("new_transaction", data);
console.log(transaction_info == response[0].transactionId)
// If transaction_info is an empty string, set the modal
if (transaction_info == '' || transaction_info == response[0].transactionId) return false;
if (transaction_info.toString() == '') return false;
else return true;
}
@@ -409,12 +434,15 @@ function App() {
// This will ensure that searchParams and transaction_info get updated on each render
socket.on("transaction_created", async (data) => {
console.log("transaction notification");
const isViewingOtherTransaction = await checkIfStillViewingOtherTransaction();
setNewTransaction(data);
if(!location.pathname.endsWith('/transactions')){
const isViewingOtherTransaction = await checkIfStillViewingOtherTransaction(data);
// If transaction_info is an empty string, set the modal
if (!isViewingOtherTransaction) {
setModal("new_transaction", data);
}
}
// Show browser notification
let permission = Notification.permission;
if (permission !== "granted") return;
@@ -429,12 +457,15 @@ function App() {
socket.on("transaction_canceled", async (data) => {
console.log("transaction notification");
const isViewingOtherTransaction = await checkIfStillViewingOtherTransaction();
setNewTransaction(data);
if(location.pathname != '/transactions'){
const isViewingOtherTransaction = await checkIfStillViewingOtherTransaction(data);
// If transaction_info is an empty string, set the modal
if (!isViewingOtherTransaction) {
setModal("new_transaction", data);
navigate(`?transactionId=${data.transactionId}`, { replace: true });
}
}
});
// Clean up the socket event listener on unmount or when dependencies change
@@ -444,17 +475,6 @@ function App() {
};
}, [socket, shopId, location]); // Ensure location is in the dependencies to respond to changes in the URL
// useEffect(() => {
// if (user.cafeId != null && user.cafeId !== shopId) {
// // Preserve existing query parameters
// const currentParams = new URLSearchParams(location.search).toString();
// // Navigate to the new cafeId while keeping existing params
// navigate(`/${user.cafeId}?${currentParams}`, { replace: true });
// }
// }, [user, shopId]);
function handleMoveToTransaction(direction, from) {
console.log(direction);
console.log(from);
@@ -472,7 +492,7 @@ function App() {
: from; // If already at the end, stay on the current transactionId
} else if (direction === 'previous') {
setDepth(currentIndex -1);
setDepth(currentIndex - 1);
// If we're not at the first transaction, get the previous transactionId
newTransactionId = currentIndex > 0
? transactionList.current[currentIndex - 1].transactionId
@@ -771,6 +791,8 @@ function App() {
sendParam={handleSetParam}
deviceType={deviceType}
paymentUrl={shop.qrPayment}
setModal={setModal}
newTransaction={newTransaction}
/>
{/* <Footer
shopId={shopIdentifier}

View File

@@ -44,10 +44,10 @@ const App = () => {
useEffect(() => {
const shopId = window.location.pathname.split('/')[1]; // Get shopId from the URL
const userId = localStorage.getItem('userId');
const user_id = localStorage.getItem('user_id');
// Connect to Socket.IO if userId is present
// if (userId) {
// Connect to Socket.IO if user_id is present
// if (user_id) {
// connectSocket(shopId, 1);
// }

View File

@@ -1,52 +0,0 @@
import React, { useState } from "react";
import styles from "./Carousel.module.css"; // Import CSS module
const Carousel = ({ selectedIndex, items, onSelect }) => {
const moveToNext = () => {
console.log('aa')
if (selectedIndex < items.length - 1) {
console.log('bb')
onSelect(selectedIndex + 1); // Send the next index to the parent
}
};
const moveToPrev = () => {
if (selectedIndex > -1) {
onSelect(selectedIndex - 1); // Send the previous index to the parent
}
};
return (
<div className={styles.carouselContainer}>
<div className={styles.carousel}>
{/* Previous Item */}
<div
className={`${styles.carouselItem} ${styles.prev}`}
onClick={moveToPrev}
style={{ visibility: selectedIndex === -1 && items.length > 0 ? "hidden" : "visible" , backgroundColor: items.length < 1 ? '#919191':'#007bff'}}
>
{selectedIndex === -1 ? (items.length > 0 ? "+" : "") : items[selectedIndex - 1]?.name || "+"}
</div>
{/* Current Item */}
<div className={`${styles.carouselItem} ${styles.center}`}>
{selectedIndex >= 0 ? items[selectedIndex]?.name : "+"}
</div>
{/* Next Item */}
<div
className={`${styles.carouselItem} ${styles.next}`}
onClick={moveToNext}
style={{
visibility: selectedIndex === items.length -1 && items.length > 0 ? "hidden" : "visible", backgroundColor: items.length < 1 ? '#919191':'#007bff'
}}
>
{selectedIndex < items.length - 1 && items[selectedIndex + 1]?.name}
</div>
</div>
</div>
);
};
export default Carousel;

View File

@@ -1,74 +0,0 @@
/* Carousel.module.css */
.carouselContainer {
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.carousel {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
max-width: 600px;
overflow: hidden;
position: relative;
}
.carouselItem {
display: flex;
justify-content: center;
align-items: center;
transition: transform 0.3s ease-in-out;
height: 200px;
background-color: #007bff;
color: white;
font-size: 24px;
width: 100%;
border-radius: 8px;
}
.carouselItem.prev,
.carouselItem.next {
transform: scale(0.8);
}
.carouselItem.center {
transform: scale(1);
width: 134%;
}
.prevBtn,
.nextBtn {
position: absolute;
top: 50%;
transform: translateY(-50%);
color: white;
border: none;
padding: 10px 20px;
font-size: 16px;
border-radius: 8px;
cursor: pointer;
}
.prevBtn {
left: 10px;
}
.nextBtn {
right: 10px;
}
@media (max-width: 600px) {
.carouselItem {
font-size: 18px;
height: 110px;
}
.prevBtn,
.nextBtn {
padding: 8px 15px;
font-size: 14px;
}
}

View File

@@ -4,6 +4,7 @@ import { useLocation } from "react-router-dom";
import { useNavigationHelpers } from "../helpers/navigationHelpers";
import Switch from "react-switch";
// Restore original gradient background for header container when shopName exists
const HeaderBarbackground = styled.div`
${({ shopName }) =>
shopName &&
@@ -19,10 +20,10 @@ const HeaderBar = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 15px;
padding: 12px 14px;
color: black;
background-color: #ffffff;
z-index: 200;
z-index: ${(props) => (props.zIndexLevel !== undefined ? props.zIndexLevel : 200)};
border: 1px solid #00000000;
margin: 20px 12px;
border-radius: 13px;
@@ -33,30 +34,15 @@ const HeaderBar = styled.div`
const Title = styled.h2`
margin: 0;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-weight: 600;
font-style: normal;
font-size:${(props) => (props.HeaderSize)};
color: rgba(88, 55, 50, 1);
text-transform: uppercase;
color: rgba(45, 45, 45, 1);
`;
const ProfileName = styled.h2`
position: absolute;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-style: normal;
font-size: 30px;
z-index: 199;
overflow: hidden;
white-space: nowrap;
animation: ${(props) => {
if (props.animate === "grow") return gg;
if (props.animate === "shrink") return ss;
return nn;
}}
0.5s forwards;
text-align: left;
`;
// SubTitle removed per redesign (no subtitle below cafe name)
// Deprecated the animated ProfileName in favor of a cleaner layout
const nn = keyframes`
0% {
@@ -103,22 +89,17 @@ const ss = keyframes`
}
`;
const ProfileImage = styled.img`
position: relative;
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: contain;
cursor: pointer;
z-index: 199;
animation: ${(props) => {
if (props.animate === "grow") return g;
if (props.animate === "shrink") return s;
return "none";
}}
0.5s forwards;
const CafeAvatar = styled.img`
width: clamp(32px, 5vw, 56px);
height: clamp(32px, 5vw, 56px);
border-radius: 8px;
object-fit: cover;
background: #f2f2f2;
margin-left: 8px; /* extra left padding so its not too tight */
`;
// User initial avatar removed; only cafe image is shown on the left
const g = keyframes`
0% {
top: 0px;
@@ -149,62 +130,43 @@ const s = keyframes`
}
`;
/* Replace bubble-like animation with subtle fade/slide */
const grow = keyframes`
0% {
right: 12px;
width: 60px;
height: 60px;
border-top-left-radius: 50%;
border-bottom-left-radius: 50%;
}
100% {
right: 28px;
width: 300px;
border-top-left-radius: 15px;
border-bottom-left-radius: 15px;
}
0% { opacity: 0; transform: translateY(-6px) scale(0.98); }
100% { opacity: 1; transform: translateY(0) scale(1); }
`;
const shrink = keyframes`
0% {
right: 12px;
width: 300px;
height: auto;
border-radius: 20px;
}
100% {
right: 28px;
width: 60px;
height: 60px;
border-radius: 50%;
}
0% { opacity: 1; transform: translateY(0) scale(1); }
100% { opacity: 0; transform: translateY(-6px) scale(0.98); }
`;
const Rectangle = styled.div`
overflow-y: hidden;
overflow-y: auto;
position: absolute;
top: 39px;
right: 12px;
width: 200px;
max-height: 87vh; /* or another appropriate value */
top: calc(100% + 8px);
right: 0;
width: 240px;
max-height: 75vh;
background-color: white;
z-index: 198;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
animation: ${(props) => (props.animate === "grow" ? grow : shrink)} 0.5s
forwards;
padding: 10px;
z-index: ${(props) => (props.baseZIndex !== undefined ? props.baseZIndex : 198)};
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
border: 1px solid #f0f0f0;
border-radius: 12px;
animation: ${(props) => (props.animate === "grow" ? grow : shrink)} 0.2s forwards;
padding: 10px 12px;
box-sizing: border-box;
overflow-x: hidden;
font-size: 14px;
color: #393939;
backdrop-filter: blur(2px);
`;
const ChildContainer = styled.div`
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-end;
flex-wrap: wrap;
padding-top: 70px;
align-items: stretch;
flex-wrap: nowrap;
`;
const ChildWrapper = styled.div`
@@ -213,25 +175,88 @@ const ChildWrapper = styled.div`
width: 100%;
`;
const Child = styled.div`
width: 100%;
height: 36px;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-style: normal;
width: 100%;
min-height: 36px;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-style: normal;
padding: ${(props) => (props.hasChildren ? '8px 0 4px' : '8px 4px')};
${(props) =>
props.hasChildren
? `
margin-top: 14px;
border-top: 0.5px solid #a5a5a5;
margin-top: 10px;
border-top: 0.5px solid #e9e9e9;
height: auto;
`
: `
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
`}
`;
const LeftGroup = styled.div`
display: flex;
align-items: center;
gap: 10px;
`;
const CenterGroup = styled.div`
flex: 1;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none; /* so clicks pass through center when needed */
`;
const CafeInfo = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
`;
const RightGroup = styled.div`
display: flex;
align-items: center;
gap: 10px;
`;
const CategoryLabel = styled.div`
font-family: "Plus Jakarta Sans", sans-serif;
font-size: 12px;
font-weight: 700;
color: #6B8F71;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 6px 2px;
pointer-events: none;
`;
const HamburgerButton = styled.button`
width: clamp(32px, 4.5vw, 52px);
height: clamp(32px, 4.5vw, 52px);
border-radius: 8px;
border: 1px solid #e5e5e5;
background: #ffffff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
& > svg {
width: 60%;
height: 60%;
}
`;
const HamburgerIcon = () => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="6" width="18" height="2" rx="1" fill="#2d2d2d"/>
<rect x="3" y="11" width="18" height="2" rx="1" fill="#2d2d2d"/>
<rect x="3" y="16" width="18" height="2" rx="1" fill="#2d2d2d"/>
</svg>
);
const Header = ({
HeaderText,
@@ -251,7 +276,9 @@ const Header = ({
removeConnectedGuestSides,
setIsEditMode,
isEditMode,
HeaderMargin = '25px'
HeaderMargin = '25px',
zIndexLevel,
rectZIndex
}) => {
const { goToLogin, goToGuestSideLogin, goToAdminCafes } =
useNavigationHelpers(shopId, tableCode);
@@ -261,10 +288,10 @@ const Header = ({
const [guestSideOf, setGuestSideOf] = useState(null);
const location = useLocation();
const handleImageClick = () => {
const toggleMenu = () => {
if (showRectangle) {
setAnimate("shrink");
setTimeout(() => setShowRectangle(false), 500);
setTimeout(() => setShowRectangle(false), 200);
} else {
setAnimate("grow");
setShowRectangle(true);
@@ -274,15 +301,14 @@ const Header = ({
const handleClickOutside = (event) => {
if (rectangleRef.current && !rectangleRef.current.contains(event.target)) {
setAnimate("shrink");
setTimeout(() => setShowRectangle(false), 500);
rectangleRef.current.style.overflow = "hidden";
setTimeout(() => setShowRectangle(false), 200);
}
};
const handleScroll = () => {
if (showRectangle) {
setAnimate("shrink");
setTimeout(() => setShowRectangle(false), 500);
setTimeout(() => setShowRectangle(false), 200);
}
};
@@ -321,52 +347,62 @@ const Header = ({
// Otherwise, use the possessive function
return `${cafeName}'s menu`;
};
const formatCafeName = (name) => {
if (!name) return name;
return name
.toLowerCase()
.replace(/\b\w/g, (c) => c.toUpperCase());
};
return (
<HeaderBarbackground shopName={shopName}>
<HeaderBar HeaderMargin={HeaderMargin} shopName={shopName}>
<Title HeaderSize={HeaderSize}>
{shopName == null
? HeaderText == null
? "kedaimaster"
: HeaderText
: shopName}
</Title>
<div style={{ visibility: showProfile ? "visible" : "hidden" }}>
<ProfileImage
src={shopImage && !shopImage.includes('undefined') ? shopImage || 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS-DjX_bGBax4NL14ULvkAdU4FP3FKoWXWu5w&s' : "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS-DjX_bGBax4NL14ULvkAdU4FP3FKoWXWu5w&s"}
alt="Profile"
onClick={user.username !== undefined ? handleImageClick : null}
animate={showRectangle && animate}
/>
<ProfileName animate={showRectangle && animate}>
{showProfile && user.username !== undefined ? user.username : "guest"}
</ProfileName>
{showRectangle && (
<Rectangle ref={rectangleRef} animate={animate}>
<ChildContainer>
<HeaderBar HeaderMargin={HeaderMargin} shopName={shopName} zIndexLevel={zIndexLevel}>
<LeftGroup>
<CafeAvatar
src={shopImage && !shopImage.includes('undefined') ? shopImage : "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS-DjX_bGBax4NL14ULvkAdU4FP3FKoWXWu5w&s"}
alt="Cafe"
/>
</LeftGroup>
<CenterGroup>
<Title HeaderSize={HeaderSize}>
{formatCafeName(
shopName == null
? HeaderText == null
? "kedaimaster"
: HeaderText
: shopName
)}
</Title>
</CenterGroup>
<RightGroup style={{ visibility: showProfile ? "visible" : "hidden", position: 'relative' }}>
<HamburgerButton onClick={toggleMenu} aria-label="Open menu">
<HamburgerIcon />
</HamburgerButton>
{showRectangle && (
<Rectangle ref={rectangleRef} animate={animate} baseZIndex={rectZIndex !== undefined ? rectZIndex : zIndexLevel}>
<ChildContainer>
{guestSideOfClerk && guestSideOfClerk.clerkUsername && (
<Child hasChildren>
this is the guest side of {guestSideOfClerk.clerkUsername}
</Child>
)}
{user.username !== undefined && (
<Child onClick={() => setModal("edit_account")}>
Kelola akun
</Child>
<CategoryLabel>Kelola akun</CategoryLabel>
)}
{user.roleId == 0 && (
<Child onClick={()=>setModal('create_coupon', {})}>Buat Voucher</Child>)}
{shopId && user.roleId == 1 && (
<Child onClick={goToAdminCafes}>Dashboard</Child>)}
{shopId &&
user.userId == shopOwnerId &&
user.user_id == shopOwnerId &&
user.username !== undefined &&
user.roleId === 1 && (
<>
<Child hasChildren>
<Child>
{shopName}
</Child>
<CategoryLabel>
{formatCafeName(shopName)}
</CategoryLabel>
<Child>
Mode pengembangan &nbsp;
<Switch
@@ -381,7 +417,7 @@ const Header = ({
</Child>
<Child hasChildren>
<Child>Konfigurasi</Child>
<CategoryLabel>Konfigurasi</CategoryLabel>
<Child onClick={() => setModal("welcome_config")}>
Desain kafe
</Child>
@@ -393,7 +429,7 @@ const Header = ({
</Child>
</Child>
<Child hasChildren>
<Child>Kasir</Child>
<CategoryLabel>Kasir</CategoryLabel>
<Child onClick={() => setModal("create_clerk")}>
+ Tambah
</Child>
@@ -420,7 +456,7 @@ const Header = ({
user.cafeId == shopId &&
user.roleId === 2 && (
<Child hasChildren>
<Child>{shopName}</Child>
<CategoryLabel>{formatCafeName(shopName)}</CategoryLabel>
<Child>
Mode pengembangan&nbsp;
@@ -435,7 +471,7 @@ const Header = ({
</Child>
<Child hasChildren>
<Child>Konfigurasi</Child>
<CategoryLabel>Konfigurasi</CategoryLabel>
<Child onClick={() => setModal("welcome_config")}>
Desain kafe
</Child>
@@ -478,11 +514,12 @@ const Header = ({
{user.username !== undefined && (
<Child hasChildren ><Child onClick={isLogout}>Logout</Child></Child>
)}
</ChildContainer>
</Rectangle>
)}
</div>
</HeaderBar></HeaderBarbackground>
</ChildContainer>
</Rectangle>
)}
</RightGroup>
</HeaderBar>
</HeaderBarbackground>
);
};

View File

@@ -0,0 +1,348 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import styles from "./IdentifyCafeModal.module.css";
import API_BASE_URL from "../config.js";
import { getTables, createTable } from "../helpers/tableHelper";
import { saveCafeDetails } from "../helpers/cafeHelpers";
import { getImageUrl } from "../helpers/itemHelper";
import { toPng } from "html-to-image";
export default function IdentifyCafeModal({ shop }) {
const [cafeIdentifyName, setCafeIdentifyName] = useState(shop.cafeIdentifyName || "");
const [availability, setAvailability] = useState(null); // 200 ok, 409 taken
const [checking, setChecking] = useState(false);
const [tables, setTables] = useState([]);
const [selectedTable, setSelectedTable] = useState(null);
const [qrSize, setQrSize] = useState(Number(shop.scale) || 1);
const [qrX, setQrX] = useState(Number(shop.xposition) || 50);
const [qrY, setQrY] = useState(Number(shop.yposition) || 50);
const [fontSize, setFontSize] = useState(Number(shop.fontsize) || 16);
const [fontColor, setFontColor] = useState(shop.fontcolor || "#FFFFFF");
const [fontX, setFontX] = useState(Number(shop.fontxposition) || 50);
const [fontY, setFontY] = useState(Number(shop.fontyposition) || 85);
const [bgImageUrl, setBgImageUrl] = useState(getImageUrl(shop.qrBackground));
const bgFileRef = useRef(null);
const [newTableNo, setNewTableNo] = useState("");
const previewRef = useRef(null);
const [copied, setCopied] = useState(false);
const [currentStep, setCurrentStep] = useState(1); // 1=Alamat, 2=Desain QR, 3=Meja
const [saveStatus, setSaveStatus] = useState(null); // 'success' | 'error'
const [saving, setSaving] = useState(false);
const initialDesignRef = useRef({
qrSize: Number(shop.scale) || 1,
qrX: Number(shop.xposition) || 50,
qrY: Number(shop.yposition) || 50,
fontSize: Number(shop.fontsize) || 16,
fontColor: shop.fontcolor || "#FFFFFF",
fontX: Number(shop.fontxposition) || 50,
fontY: Number(shop.fontyposition) || 85,
bgImageUrl: getImageUrl(shop.qrBackground),
});
const shopHost = useMemo(() => window.location.hostname, []);
const fullLink = useMemo(() => `${shopHost}/${cafeIdentifyName}`, [shopHost, cafeIdentifyName]);
// Debounced availability check
const debounceRef = useRef(null);
const handleIdentifyChange = (e) => {
const val = e.target.value
.toLowerCase()
.replace(/\s+/g, "_")
.replace(/[^a-z0-9_]/g, "");
setCafeIdentifyName(val);
setChecking(true);
setAvailability(null);
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(async () => {
try {
const res = await fetch(`${API_BASE_URL}/cafe/check-identifyName/${val}`);
setAvailability(res.ok ? 200 : 409);
} catch (_) {
setAvailability(409);
} finally {
setChecking(false);
}
}, 600);
};
// Load tables
useEffect(() => {
(async () => {
try {
const fetched = await getTables(shop.cafeId);
setTables(fetched || []);
} catch (e) {
// ignore
}
})();
}, [shop.cafeId]);
const handleUploadBg = (e) => {
const file = e.target.files?.[0];
if (file) {
const url = URL.createObjectURL(file);
setBgImageUrl(url);
}
};
const handleCreateTable = async () => {
if (!newTableNo) return;
try {
const created = await createTable(shop.cafeId, { tableNo: newTableNo });
setTables((t) => [...t, created]);
setNewTableNo("");
} catch (e) {
// noop
}
};
const handleSave = async () => {
setSaving(true);
setSaveStatus(null);
const qrBackgroundFile = bgFileRef.current?.files?.[0];
const details = {
qrSize,
qrPosition: { left: qrX, top: qrY },
qrBackgroundFile,
fontsize: fontSize,
fontcolor: fontColor,
fontPosition: { left: fontX, top: fontY },
cafeIdentifyName: shop.cafeIdentifyName !== cafeIdentifyName ? cafeIdentifyName : null,
};
try {
await saveCafeDetails(shop.cafeId, details);
setSaveStatus('success');
} catch (e) {
setSaveStatus('error');
} finally {
setSaving(false);
}
};
const downloadPreview = async () => {
if (!previewRef.current) return;
const node = previewRef.current;
const originalBg = node.style.backgroundColor;
node.style.backgroundColor = "transparent";
try {
const dataUrl = await toPng(node, { pixelRatio: 2 });
const link = document.createElement("a");
link.href = dataUrl;
link.download = selectedTable ? `QR Meja (${selectedTable.tableNo}).png` : `QR ${shop.name}.png`;
link.click();
} catch (e) {
// noop
} finally {
node.style.backgroundColor = originalBg;
}
};
const copyLink = async () => {
try {
await navigator.clipboard.writeText(fullLink);
setCopied(true);
setTimeout(()=>setCopied(false), 1400);
} catch (_) {}
};
const applyPreset = (preset) => {
if (preset === 'center') {
setQrX(50); setQrY(50); setQrSize(1);
setFontX(50); setFontY(85);
} else if (preset === 'topLeft') {
setQrX(25); setQrY(30); setQrSize(1);
setFontX(50); setFontY(85);
} else if (preset === 'bottomRight') {
setQrX(75); setQrY(70); setQrSize(1);
setFontX(50); setFontY(15);
}
};
const resetDesign = () => {
const d = initialDesignRef.current;
setQrSize(d.qrSize);
setQrX(d.qrX);
setQrY(d.qrY);
setFontSize(d.fontSize);
setFontColor(d.fontColor);
setFontX(d.fontX);
setFontY(d.fontY);
setBgImageUrl(d.bgImageUrl);
if (bgFileRef.current) bgFileRef.current.value = '';
};
// Positioning helpers
const qrStyle = {
left: `${qrX}%`,
top: `${qrY}%`,
transform: `translate(-50%, -50%) scale(${qrSize})`,
};
const fontStyle = {
left: `${fontX}%`,
top: `${fontY}%`,
transform: `translate(-50%, -50%)`,
color: fontColor,
fontSize: `${fontSize}px`,
position: "absolute",
fontWeight: 700,
};
const qrData = selectedTable ? `${fullLink}/${selectedTable.tableNo}` : fullLink;
const canProceedFromStep1 = () => {
if (!cafeIdentifyName) return false;
if (cafeIdentifyName === (shop.cafeIdentifyName || '')) return true;
return availability === 200 && !checking;
};
return (
<div className={styles.container}>
<div className={styles.header}>
<div className={styles.title}>Identifikasi kedai</div>
<div className={styles.steps}>
<div className={`${styles.step} ${currentStep === 1 ? styles.stepActive : ''}`}>
<span className={styles.stepNumber}>1</span> Alamat
</div>
<div className={`${styles.step} ${currentStep === 2 ? styles.stepActive : ''}`}>
<span className={styles.stepNumber}>2</span> Desain QR
</div>
<div className={`${styles.step} ${currentStep === 3 ? styles.stepActive : ''}`}>
<span className={styles.stepNumber}>3</span> Meja
</div>
</div>
</div>
<div className={styles.scrollArea}>
{currentStep === 1 && (
<div className={styles.section}>
<div className={styles.sectionTitle}>Alamat kedai</div>
<div className={styles.row}>
<div className={styles.domainPrefix}>{shopHost}/</div>
<input
value={cafeIdentifyName}
onChange={handleIdentifyChange}
className={styles.input}
placeholder="alamat_kedai"
/>
<div className={`${styles.status} ${availability === 200 ? styles.statusOk : availability === 409 ? styles.statusBad : ''}`}>
{checking ? 'Memeriksa…' : availability === 200 ? 'Tersedia' : availability === 409 ? 'Terpakai' : 'Menunggu input'}
</div>
</div>
<div className={styles.helpText}>Gunakan huruf kecil, angka, dan garis bawah (_). Contoh: kopikenangan_malam</div>
<div className={styles.copyRow}>
<input className={styles.linkField} readOnly value={fullLink} />
<button className={styles.button} onClick={copyLink}>{copied ? 'Disalin' : 'Salin'}</button>
</div>
</div>
)}
{currentStep === 2 && (
<div className={styles.section}>
<div className={styles.sectionTitle}>Desain QR</div>
<div className={styles.presets}>
<button className={styles.presetButton} onClick={()=>applyPreset('center')}>Tengah</button>
<button className={styles.presetButton} onClick={()=>applyPreset('topLeft')}>Atas-Kiri</button>
<button className={styles.presetButton} onClick={()=>applyPreset('bottomRight')}>Bawah-Kanan</button>
<button className={styles.presetButton} onClick={resetDesign}>Reset desain</button>
</div>
<div className={styles.grid}>
<div>
<div ref={previewRef} id="qr-code-container" className={styles.previewBox} style={{ backgroundColor: '#fff' }}>
{bgImageUrl && <img src={bgImageUrl} alt="Background" className={styles.bgPreview} />}
<img
className={styles.qrLayer}
style={qrStyle}
alt="QR"
src={`https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${encodeURIComponent(qrData)}`}
/>
<div style={fontStyle}>{selectedTable ? selectedTable.tableNo : 'Kedai'}</div>
</div>
</div>
<div className={styles.controls}>
<div className={styles.field}>
<label className={styles.label}>Gambar latar</label>
<input ref={bgFileRef} type="file" accept="image/*" onChange={handleUploadBg} />
</div>
<div className={styles.field}>
<label className={styles.label}>Skala QR ({qrSize}x)</label>
<input className={styles.range} type="range" min="0.5" max="3" step="0.1" value={qrSize} onChange={(e)=>setQrSize(parseFloat(e.target.value))} />
</div>
<div className={styles.field}>
<label className={styles.label}>Posisi QR - horizontal ({qrX}%)</label>
<input className={styles.range} type="range" min="0" max="100" step="1" value={qrX} onChange={(e)=>setQrX(parseInt(e.target.value))} />
</div>
<div className={styles.field}>
<label className={styles.label}>Posisi QR - vertikal ({qrY}%)</label>
<input className={styles.range} type="range" min="0" max="100" step="1" value={qrY} onChange={(e)=>setQrY(parseInt(e.target.value))} />
</div>
<div className={styles.field}>
<label className={styles.label}>Ukuran teks ({fontSize}px)</label>
<input className={styles.range} type="range" min="8" max="48" step="1" value={fontSize} onChange={(e)=>setFontSize(parseInt(e.target.value))} />
</div>
<div className={styles.field}>
<label className={styles.label}>Warna teks</label>
<input className={styles.colorInput} type="color" value={fontColor} onChange={(e)=>setFontColor(e.target.value)} />
</div>
<div className={styles.field}>
<label className={styles.label}>Posisi teks - horizontal ({fontX}%)</label>
<input className={styles.range} type="range" min="0" max="100" step="1" value={fontX} onChange={(e)=>setFontX(parseInt(e.target.value))} />
</div>
<div className={styles.field}>
<label className={styles.label}>Posisi teks - vertikal ({fontY}%)</label>
<input className={styles.range} type="range" min="0" max="100" step="1" value={fontY} onChange={(e)=>setFontY(parseInt(e.target.value))} />
</div>
</div>
</div>
<div className={styles.helpText}>Tip: Gunakan latar yang kontras agar QR mudah dipindai.</div>
</div>
)}
{currentStep === 3 && (
<div className={styles.section}>
<div className={styles.sectionTitle}>Daftar meja</div>
<div className={styles.tables}>
<input className={styles.input} placeholder="Meja A1" value={newTableNo} onChange={(e)=>setNewTableNo(e.target.value)} />
<button className={styles.button} onClick={handleCreateTable}>Buat meja</button>
</div>
<div className={styles.tableList}>
{tables && tables
.filter((t)=>t.tableNo !== 0)
.map((t)=>{
const active = selectedTable && selectedTable.tableId === t.tableId;
return (
<div
key={t.tableId}
className={`${styles.tableItem} ${active ? styles.tableItemActive : ''}`}
onClick={()=>setSelectedTable(active ? null : t)}
>
{t.tableNo}
</div>
);
})}
</div>
<div className={styles.helpText}>Pilih meja untuk membuat QR khusus meja (opsional).</div>
</div>
)}
</div>
<div className={styles.footer}>
<div className={styles.actions}>
<button className={`${styles.button} ${styles.secondary}`} disabled={currentStep === 1} onClick={()=>setCurrentStep((s)=>Math.max(1, s-1))}>Kembali</button>
{currentStep < 3 ? (
<button className={`${styles.button} ${styles.primary}`} disabled={currentStep === 1 && !canProceedFromStep1()} onClick={()=>setCurrentStep((s)=>Math.min(3, s+1))}>Lanjut</button>
) : (
<button className={`${styles.button} ${styles.primary}`} onClick={handleSave} disabled={saving}>{saving ? 'Menyimpan…' : 'Simpan'}</button>
)}
</div>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
{saveStatus === 'success' && <div className={`${styles.banner} ${styles.bannerSuccess}`}>Simpan berhasil</div>}
{saveStatus === 'error' && <div className={`${styles.banner} ${styles.bannerError}`}>Gagal menyimpan</div>}
<button className={`${styles.button} ${styles.muted}`} onClick={downloadPreview}>Download QR {selectedTable ? 'meja' : 'kedai'}</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,299 @@
/* IdentifyCafeModal.module.css */
.container {
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px;
background: #fff;
height: 100%;
}
.steps {
display: flex;
gap: 8px;
align-items: center;
}
.step {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 999px;
border: 1px solid #e6e6e6;
background: #fafafa;
color: #666;
font-weight: 600;
font-size: 13px;
}
.stepNumber {
width: 22px;
height: 22px;
border-radius: 50%;
background: #e9ecef;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
.stepActive {
border-color: #cdebd8;
background: #e9f7ef;
color: #245c3d;
}
.helpText {
font-size: 12px;
color: #666;
margin-top: 6px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
.title {
font-size: 18px;
font-weight: 700;
}
.scrollArea {
overflow-y: auto;
overflow-x: hidden;
}
.section {
background: #fafafa;
border: 1px solid #e6e6e6;
border-radius: 12px;
padding: 16px;
}
.sectionTitle {
margin: 0 0 10px 0;
font-weight: 600;
font-size: 15px;
}
.row {
display: flex;
gap: 12px;
align-items: center;
}
.domainPrefix {
background: #f0f0f0;
border: 1px solid #ddd;
border-radius: 10px 0 0 10px;
padding: 10px 12px;
font-size: 14px;
color: #555;
}
.input {
flex: 1;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 0 10px 10px 0;
outline: none;
font-size: 14px;
}
.status {
font-size: 12px;
font-weight: 700;
padding: 6px 10px;
border-radius: 999px;
}
.statusOk {
color: #155724;
background: #d4edda;
border: 1px solid #c3e6cb;
}
.statusBad {
color: #721c24;
background: #f8d7da;
border: 1px solid #f5c6cb;
}
.copyRow {
display: flex;
gap: 8px;
align-items: center;
margin-top: 10px;
}
.linkField {
flex: 1;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 10px;
font-size: 14px;
color: #333;
background: #fff;
}
.button {
padding: 10px 14px;
border-radius: 10px;
border: 1px solid #ddd;
background: #fff;
cursor: pointer;
}
.button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.grid {
display: grid;
grid-template-columns: 1.3fr 1fr;
gap: 16px;
}
.previewBox {
border: 1px solid #e6e6e6;
border-radius: 12px;
background: #fff;
height: 280px;
position: relative;
overflow: hidden;
}
.bgPreview {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.qrLayer {
position: absolute;
}
.controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.presets {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 8px 0 4px 0;
}
.presetButton {
padding: 6px 10px;
border-radius: 8px;
border: 1px solid #e6e6e6;
background: #fff;
font-size: 12px;
cursor: pointer;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
.label {
font-size: 12px;
color: #555;
}
.range {
width: 100%;
}
.colorInput {
height: 38px;
padding: 0 6px;
}
.tables {
display: grid;
grid-template-columns: 1fr auto;
gap: 10px;
align-items: center;
}
.tableList {
max-height: 180px;
overflow: auto;
border: 1px solid #e6e6e6;
border-radius: 8px;
padding: 8px;
}
.tableItem {
padding: 8px 10px;
border-radius: 6px;
cursor: pointer;
}
.tableItemActive {
background: #e9f7ef;
border: 1px solid #cdebd8;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.primary {
background: #28a745;
color: #fff;
border: none;
}
.muted {
background: #f6f6f6;
}
.actions {
display: flex;
gap: 8px;
}
.secondary {
background: #f0f0f0;
}
.banner {
padding: 10px 12px;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
}
.bannerSuccess {
color: #155724;
background: #d4edda;
border: 1px solid #c3e6cb;
}
.bannerError {
color: #721c24;
background: #f8d7da;
border: 1px solid #f5c6cb;
}
@media (max-width: 820px) {
.grid { grid-template-columns: 1fr; }
.controls { grid-template-columns: 1fr; }
}

View File

@@ -4,6 +4,8 @@ const Item = ({
blank,
forCart,
forInvoice,
portrait,
hideDetails,
name: initialName,
description: initialDescription,
price: initialPrice,
@@ -53,10 +55,10 @@ const Item = ({
};
const handleCreate = () => {
handleCreateItem(itemName, itemPrice, selectedImage, previewUrl);
handleCreateItem(itemName, itemPrice, selectedImage, previewUrl, itemDescription, promoPrice);
};
const handleUpdate = () => {
handleUpdateItem(itemName, itemPrice, selectedImage, previewUrl);
handleUpdateItem(itemName, itemPrice, selectedImage, itemDescription, promoPrice);
};
const handleRemoveClick = () => {
@@ -74,6 +76,11 @@ const Item = ({
}
};
const formatCurrency = (value) => {
const num = Number(value) || 0;
return num.toLocaleString('id-ID');
};
const handlePriceChange = (event) => {
setItemPrice(event.target.value);
};
@@ -89,245 +96,73 @@ const Item = ({
setItemName(event.target.value);
};
const toTitleCase = (str) => {
if (!str) return str;
return String(str)
.toLowerCase()
.replace(/(^|[\s\-/'])([a-zA-Z\u00C0-\u024F])/g, (m, p1, p2) => p1 + p2.toUpperCase());
};
const displayName = toTitleCase(itemName);
return (
<div className={`${!last && !forInvoice ? styles.notLast : ""}`}>
<div className={`${styles.item} ${forInvoice ? styles.itemInvoice : ""} `}>
<div className={`${(!portrait && !last && !forInvoice) ? styles.notLast : ""}`}>
<div className={`${styles.item} ${forInvoice ? styles.itemInvoice : ""} ${portrait ? styles.itemPortrait : ""} `}>
{!forInvoice && (
// <div className={styles.imageContainer}>
<img
src={
previewUrl
}
onError={({ currentTarget }) => {
currentTarget.onerror = null; // prevents looping
currentTarget.src =
"https://png.pngtree.com/png-vector/20221125/ourmid/pngtree-no-image-available-icon-flatvector-illustration-pic-design-profile-vector-png-image_40966566.jpg";
}}
alt={itemName}
style={{
filter: !isAvailable ? "grayscale(100%)" : "none",
}}
className={styles.imageContainer}
/>
// </div>
<div className={styles.imageWrap}>
<img
src={previewUrl}
onError={({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src =
"https://png.pngtree.com/png-vector/20221125/ourmid/pngtree-no-image-available-icon-flatvector-illustration-pic-design-profile-vector-png-image_40966566.jpg";
}}
alt={itemName}
style={{ filter: !isAvailable ? "grayscale(100%)" : "none" }}
className={styles.imageContainer}
/>
{promoPrice && promoPrice != 0 && promoPrice != '' && (
<div className={styles.promoPill}>Promo</div>
)}
{portrait && (
<>
<div className={styles.overlayName}>{displayName}</div>
<div className={styles.bottomOverlay}>
<div className={styles.overlayPriceCol}>
{(promoPrice && promoPrice != 0 && promoPrice != '') ? (
<>
<span className={styles.overlayOriginal}>Rp {formatCurrency(initialPrice)}</span>
<span className={styles.overlayPromo}>Rp {formatCurrency(promoPrice)}</span>
</>
) : (
<span className={styles.overlayPromo}>Rp {formatCurrency(initialPrice)}</span>
)}
</div>
<div className={styles.qtyGroup}>
<button className={styles.qtyBtn} onClick={handleNegativeClick} aria-label="Kurangi">-</button>
<span className={styles.qtyVal}>{itemQty}</span>
<button className={styles.qtyBtn} onClick={handlePlusClick} aria-label="Tambah">+</button>
</div>
</div>
</>
)}
</div>
)}
{forInvoice && (
<div className={styles.itemDetails}>
<div className={styles.plusNegative2} onClick={onRemoveClick}></div>
<p className={styles.multiplySymbol}>x</p>
<p className={styles.qtyInvoice}>{itemQty}</p>
<p className={styles.itemPriceInvoice}>Rp {formatCurrency(itemQty * (promoPrice > 0 ? promoPrice : itemPrice))}</p>
</div>
)}
<div className={styles.itemDetails}>
{forInvoice &&
<svg
className={styles.plusNegative2}
onClick={onRemoveClick}
clipRule="evenodd"
fillRule="evenodd"
strokeLinejoin="round"
strokeMiterlimit="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12.002 2c5.518 0 9.998 4.48 9.998 9.998 0 5.517-4.48 9.997-9.998 9.997-5.517 0-9.997-4.48-9.997-9.997 0-5.518 4.48-9.998 9.997-9.998zm0 1.5c-4.69 0-8.497 3.808-8.497 8.498s3.807 8.497 8.497 8.497 8.498-3.807 8.498-8.497-3.808-8.498-8.498-8.498zm-.747 7.75h-3.5c-.414 0-.75.336-.75.75s.336.75.75.75h3.5v3.5c0 .414.336.75.75.75s.75-.336.75-.75v-3.5h3.5c.414 0 .75-.336.75-.75s-.336-.75-.75-.75h-3.5v-3.5c0-.414-.336-.75-.75-.75s-.75.336-.75.75z"
fillRule="nonzero"
/>
</svg>
}
{/* <input
className={`${
forInvoice ? styles.itemInvoiceName : styles.itemName
} ${isBeingEdit || blank ? styles.blank : styles.notblank} ${
!isAvailable ? styles.disabled : ""
}`}
value={itemName}
placeholder="Nama item"
onChange={handleNameChange}
disabled={!blank && !isBeingEdit}
/> */}
<h3 style={{
textTransform: 'capitalize',
margin: `${forInvoice ? '13px 0px 10px 10px' : '5px 0px 10px 10px'}`,
fontSize: '16px',
display: '-webkit-box',
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
WebkitLineClamp: 2,
textOverflow: 'ellipsis',
width: `${forInvoice? '160px' : 'unset'}`
}}>
{itemName}
</h3>
{forInvoice && (
<>
<p className={styles.multiplySymbol}>x</p>
<p className={styles.qtyInvoice}>{itemQty}</p>
</>
)}
{!forInvoice && (
// <input
// className={`${styles.itemPrice} ${
// isBeingEdit || blank ? styles.blank : styles.notblank
// } ${!isAvailable ? styles.disabled : ""}`}
// value={itemPrice}
// placeholder="Harga"
// onChange={handlePriceChange}
// disabled={!blank && !isBeingEdit}
// />
<div style={{
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
color: 'rgb(28, 29, 29)',
fontSize: '0.875rem',
fontWeight: 600,
lineHeight: '1rem',
marginLeft: 10
}}>
{promoPrice && promoPrice != 0 && promoPrice != '' ?
<>
<div style={{
position: 'relative',
marginTop: '0.125rem',
display: 'flex',
width: '87px',
alignItems: 'center',
whiteSpace: 'nowrap',
borderRadius: '9999px',
backgroundColor: !isAvailable ? 'gray' : 'unset',
backgroundImage: isAvailable && 'linear-gradient(to right, #e52535, #fe6d78)',
padding: '0.25rem 0rem',
color: 'rgb(255, 255, 255)',
fontSize: '0.75rem',
fontWeight: 600,
lineHeight: '1rem',
justifyContent: 'center'
}}>
Promo {(((initialPrice - promoPrice) / initialPrice) * 100).toFixed(0)}%
</div>
<div style={{ display: 'flex' }}>
<span style={{
marginLeft: '1rem',
marginRight: '0.5rem',
marginTop: '0.125rem'
}}>{promoPrice}</span>
<span style={{
marginTop: '0.125rem',
color: 'rgb(114, 114, 114)',
textDecoration: 'line-through'
}}>{initialPrice}</span>
</div>
</>
:
<>
<div style={{ display: 'flex' }}>
<span style={{
marginRight: '0.5rem',
marginTop: '0.125rem'
}}>{initialPrice}</span>
</div>
</>
}
</div>
)}
{!forInvoice &&
(!isBeingEdit && itemQty != 0 ? (
<div className={styles.itemQty}>
<svg
className={styles.plusNegative}
onClick={handleNegativeClick}
clipRule="evenodd"
fillRule="evenodd"
strokeLinejoin="round"
strokeMiterlimit="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12.002 2.005c5.518 0 9.998 4.48 9.998 9.997 0 5.518-4.48 9.998-9.998 9.998-5.517 0-9.997-4.48-9.997-9.998 0-5.517 4.48-9.997 9.997-9.997zm0 1.5c-4.69 0-8.497 3.807-8.497 8.497s3.807 8.498 8.497 8.498 8.498-3.808 8.498-8.498-3.808-8.497-8.498-8.497zm4.253 7.75h-8.5c-.414 0-.75.336-.75.75s.336.75.75.75h8.5c.414 0 .75-.336.75-.75s-.336-.75-.75-.75z"
fillRule="nonzero"
/>
</svg>
{!blank && !isBeingEdit ? (
<p className={styles.itemQtyValue}>{itemQty}</p>
) : (
<input
className={styles.itemQtyInput}
value={itemQty}
onChange={handleQtyChange}
disabled={!blank && !isBeingEdit}
/>
)}
<svg
className={styles.plusNegative}
onClick={handlePlusClick}
clipRule="evenodd"
fillRule="evenodd"
strokeLinejoin="round"
strokeMiterlimit="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12.002 2c5.518 0 9.998 4.48 9.998 9.998 0 5.517-4.48 9.997-9.998 9.997-5.517 0-9.997-4.48-9.997-9.997 0-5.518 4.48-9.998 9.997-9.998zm0 1.5c-4.69 0-8.497 3.808-8.497 8.498s3.807 8.497 8.497 8.497 8.498-3.807 8.498-8.497-3.808-8.498-8.498-8.498zm-.747 7.75h-3.5c-.414 0-.75.336-.75.75s.336.75.75.75h3.5v3.5c0 .414.336.75.75.75s.75-.336.75-.75v-3.5h3.5c.414 0 .75-.336.75-.75s-.336-.75-.75-.75h-3.5v-3.5c0-.414-.336-.75-.75-.75s-.75.336-.75.75z"
fillRule="nonzero"
/>
</svg>
</div>
) : !blank && !isBeingEdit ? (
<div className={styles.itemQty}>
<button
className={styles.addButton}
style={{ backgroundColor: !isAvailable ? "" : "inherit", border: `2px solid ${isAvailable ? 'inherit' : 'gray'}`, color: `${isAvailable ? '#a8c7a9' : 'gray'}` }}
onClick={handlePlusClick}
disabled={!isAvailable} // Optionally disable the button if not available
>
Pesan
</button>
</div>
) : (
<div className={styles.itemQty}>
<button
className={styles.addButton}
style={{
backgroundColor: "white",
width: "150px",
color: '#a8c7a9'
}}
onClick={isBeingEdit ? handleUpdate : handleCreate}
>
{isBeingEdit ? "Simpan" : "Buat"}
</button>
</div>
))}
{forInvoice && (
<p className={styles.itemPriceInvoice}>Rp {itemQty * (promoPrice > 0? promoPrice : itemPrice)}</p>
)}
</div>
{forCart && (
<div className={styles.remove} onClick={handleRemoveClick}>
</div>
)}
{/* {blank && (
<button className={styles.createItem} onClick={handleCreate}>
Create Item
</button>
)} */}
</div>
{itemDescription && itemDescription != 'undefined' && itemDescription?.length &&
<div>
<p style={{
maxHeight: '34px',
display: '-webkit-box',
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
WebkitLineClamp: 2,
textOverflow: 'ellipsis', color: '#5f5f5f', fontSize: '14px', padding: '5px', margin: 0
}}>{itemDescription}</p>
</div>
}
{null}
</div>
);
};

View File

@@ -1,32 +1,100 @@
.itemContainer {
display: flex;
flex-direction: column;
/* gap: 10px; */
}
.item {
display: flex;
align-items: stretch;
align-items: center;
justify-content: space-between;
padding-left: 5px;
margin-top: 5px;
margin-bottom: 5px;
color: rgba(88, 55, 50, 1);
font-size: 32px;
box-sizing: border-box; /* Include padding and border in the element's total width */
width: 100%; /* Ensure the item does not exceed the parent's width */
overflow: hidden; /* Prevent internal overflow */
padding-top: 10px;
margin-bottom: 5px;
width: 100%;
gap: 12px;
padding: 16px;
margin: 0;
border: none;
border-radius: 12px;
background: #ffffff;
box-shadow: 0 1px 5px rgba(0,0,0,0.08);
box-sizing: border-box;
transition: all 0.2s ease;
position: relative;
}
.item:not(.itemInvoice) {
/* border-top: 2px solid #00000017; */
.item:hover { /* remove hover effect on list items */ }
/* Portrait variant for cafe page grid */
.itemPortrait {
flex-direction: column;
align-items: stretch;
padding: 0;
margin: 0;
border: none;
background: transparent;
box-shadow: none;
border-radius: 12px;
overflow: hidden;
}
.notLast{
padding-bottom: 10px;
border-bottom: 2px solid #00000017;
.imageWrap {
position: relative;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
width: 80px;
height: 80px;
flex-shrink: 0;
}
.itemPortrait .imageWrap {
width: 100%;
height: auto;
aspect-ratio: 1 / 1;
border-radius: 12px;
}
.imageContainer {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.itemPortrait .imageContainer {
border-radius: 12px;
}
.itemDetails {
display: flex;
flex-direction: column;
justify-content: center;
align-items: stretch;
gap: 8px;
flex: 1;
min-width: 0;
}
.itemPortrait .itemDetails {
align-items: flex-start;
text-align: left;
gap: 8px;
padding: 12px 0;
}
.itemPortrait .itemQty {
justify-content: flex-start;
}
.itemPortrait .title {
font-size: 15px;
}
.itemPortrait .priceNow {
font-size: 14px;
}
.notLast {
border-bottom: none;
padding-bottom: 16px;
}
.itemInvoice {
@@ -37,10 +105,14 @@
margin-top: 0px;
margin-bottom: 0px;
padding-top: 0px;
padding: 16px;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 1px 5px rgba(0,0,0,0.08);
}
.itemInvoice:last-child {
margin-bottom: 0; /* Remove margin-bottom for the last child */
margin-bottom: 0;
}
.itemImage {
@@ -48,29 +120,30 @@
height: 100%;
}
.imageContainer {
.item:not(.itemPortrait) .imageContainer {
position: relative;
width: 26vw;
height: 26vw;
border-radius: 12px;
width: 80px;
height: 80px;
border-radius: 10px;
object-fit: cover;
}
.overlay {
position: absolute;
top: 15px;
left: 8px;
right: 8px;
bottom: 15px;
top: 16px;
left: 12px;
right: 12px;
bottom: 16px;
background-color: rgba(0, 0, 0, 0.5);
color: white;
display: flex;
justify-content: center;
align-items: center;
border-radius: 20px;
border-radius: 16px;
cursor: pointer;
transition: background-color 0.3s ease;
font-size: 3.3vw;
font-size: 16px;
font-weight: 500;
}
.overlay:hover {
@@ -81,20 +154,18 @@
display: none;
}
.itemDetails {
.infoRow {
display: flex;
flex-direction: column;
align-items: baseline;
justify-content: space-between;
margin-left: 10px;
margin-right: 10px;
flex-grow: 1;
gap: 12px;
}
.itemInvoiceDetails {
display: flex;
flex-direction: column;
justify-content: space-between;
margin-left: 10px;
margin-left: 12px;
margin-top: -15px;
flex-grow: 1;
}
@@ -102,50 +173,47 @@
.itemName {
font-family: "Plus Jakarta Sans", sans-serif;
font-style: normal;
width: calc(100% - 15px); /* Adjust the width to prevent overflow */
font-size: 5vw;
font-weight: 500;
width: calc(100% - 20px);
font-size: 16px;
font-weight: 600;
margin-top: 0;
margin: 0 5px;
color: rgba(88, 55, 50, 1);
margin: 0 6px;
color: #333;
background-color: transparent;
text-transform: capitalize;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.itemInvoiceName {
width: calc(260% - 15px);
width: calc(260% - 20px);
background-color: transparent;
font-size: 1.3rem;
font-weight: 500;
font-size: 1.2rem;
font-weight: 600;
}
.multiplySymbol {
font-weight: 600;
color: #555;
}
.qtyInvoice {
font-weight: 500;
color: #555;
}
.itemPrice {
font-family: "Plus Jakarta Sans", sans-serif;
font-style: normal;
font-weight: 600;
width: calc(100% - 15px); /* Adjust the width to prevent overflow */
font-size: 3.3vw;
/* margin-bottom: 35px; */
margin-left: 5px;
color: #3a3a3a;
background-color: transparent;
display: none;
}
.itemPriceInvoice {
font-family: "Plus Jakarta Sans", sans-serif;
font-style: normal;
font-weight: 600;
font-size: 0.9rem;
margin-left: 5px;
color: #d9c61c;
font-weight: 700;
font-size: 1rem;
margin-left: 6px;
color: #6B8F71;
text-align: right;
margin-top: 22px;
}
@@ -153,75 +221,404 @@
.itemQty {
display: flex;
align-items: center;
font-size: 0.9rem;
margin-left: 5px;
color: #a8c7a9;
fill: #a8c7a9;
height: 40px;
justify-content: flex-end;
gap: 12px;
min-height: 40px;
}
.itemQtyValue {
font-family: "Plus Jakarta Sans", sans-serif;
font-style: normal;
font-weight: 600;
margin-top: 19px;
margin-left: 1px;
margin-right: 1px;
width: 25px;
font-weight: 700;
margin-top: 22px;
margin-left: 2px;
margin-right: 2px;
width: 32px;
text-align: center;
color: #333;
}
.itemQtyInput {
font-family: "Plus Jakarta Sans", sans-serif;
font-style: normal;
font-weight: 600;
width: 30px; /* Adjust the width to prevent overflow */
font-size: 0.9rem;
margin-bottom: 10px;
font-weight: 700;
width: 40px;
font-size: 1rem;
margin-bottom: 12px;
text-align: center;
background-color: transparent;
border: 1px solid #ddd;
border-radius: 8px;
padding: 8px;
}
.addButton {
background-color: #ffffff;
border: 2px solid #a8c7a9;
/* border: none; */
display: inline-block;
font-size: 14px;
font-weight: 600;
cursor: pointer;
width: 87px;
height: 32px;
margin-left: 5px;
margin-top: 5px;
border-radius: 20px;
background-color: var(--brand-sage, #6B8F71);
border: 1px solid var(--brand-sage, #6B8F71);
color: #ffffff;
display: inline-block;
font-size: 14px;
font-weight: 600;
cursor: pointer;
min-width: 90px;
height: 40px;
padding: 0 16px;
border-radius: 10px;
box-shadow: 0 2px 6px rgba(107, 143, 113, 0.2);
transition: all 0.2s ease;
}
.addButton:hover {
background-color: var(--brand-sage-hover, #5a7a60);
border-color: var(--brand-sage-hover, #5a7a60);
box-shadow: 0 4px 10px rgba(107, 143, 113, 0.3);
}
.addButton:disabled {
background-color: var(--brand-sage-muted, #CFD8D3);
border-color: var(--brand-sage-muted, #CFD8D3);
cursor: default;
box-shadow: none;
}
.grayscale {
filter: grayscale(100%);
}
.disabled {
color: gray;
color: #999;
}
.plusNegative {
width: 35px;
height: 35px;
margin: 2.5px 0 -0.5px 0px;
width: 40px;
height: 40px;
border-radius: 10px;
border: 1px solid #ddd;
background: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 700;
color: #666;
cursor: pointer;
transition: all 0.2s ease;
}
.plusNegative:hover {
background: #e9ecef;
border-color: #ccc;
}
.plusNegative2 {
width: 84px;
height: 21px;
width: 40px;
height: 40px;
position: absolute;
transform: rotate(45deg);
left: -33px;
top: 21px;
right: 16px;
top: 50%;
transform: translateY(-50%);
border-radius: 10px;
border: 1px solid #ddd;
background: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 700;
color: #666;
cursor: pointer;
transition: all 0.2s ease;
}
.plusNegative2:hover {
background: #e9ecef;
border-color: #ccc;
}
.remove {
width: 25px;
height: 25px;
margin-top: -10px;
margin-right: 10px;
width: 32px;
height: 32px;
position: absolute;
top: 12px;
right: 12px;
border-radius: 50%;
background: #ff4d4d;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 6px rgba(255, 77, 77, 0.3);
}
.remove:hover {
background: #ff1a1a;
transform: scale(1.1);
}
/* New elements for clean cafe item card */
.title {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;
font-size: 17px;
color: #333;
margin: 0 0 4px 0;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 2;
text-transform: capitalize;
text-align: left;
flex: 1;
min-width: 0;
}
/* Responsive type scale for title and price */
@media (min-width: 600px) {
.title {
font-size: 18px;
}
.priceNow {
font-size: 16px;
}
}
@media (min-width: 992px) {
.title {
font-size: 19px;
}
.priceNow {
font-size: 17px;
}
}
.desc {
color: #666;
font-size: 13px;
line-height: 1.4;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 2;
}
.priceRow {
display: inline-flex;
align-items: center;
gap: 10px;
}
.promoBadge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 4px 10px;
height: 24px;
border-radius: 999px;
background: linear-gradient(to right, #e52535, #fe6d78);
color: #fff;
font-size: 12px;
font-weight: 700;
}
.priceNow {
color: #1c1d1d;
font-weight: 700;
font-size: 15px;
white-space: nowrap;
}
.priceOld {
color: #727272;
font-size: 13px;
text-decoration: line-through;
white-space: nowrap;
}
.qtyGroup {
display: inline-flex;
align-items: center;
border: 1px solid #ddd;
border-radius: 10px;
height: 36px;
overflow: hidden;
background: #ffffff;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.qtyBtn {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: #f8f9fa;
color: #333;
border: none;
font-size: 18px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
}
.qtyBtn:hover {
background: #e9ecef;
}
.qtyVal {
min-width: 30px;
text-align: center;
font-weight: 700;
color: #333;
font-size: 16px;
}
.actionRow {
display: flex;
align-items: center;
gap: 10px;
justify-content: space-between;
width: 100%;
}
/* Promo pill overlay on image */
.promoPill {
position: absolute;
top: 10px;
right: 10px;
z-index: 2;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 6px 12px;
height: auto;
border-radius: 8px;
background: #e53935;
color: #fff;
font-size: 12px;
font-weight: 700;
box-shadow: 0 2px 6px rgba(229, 57, 53, 0.3);
}
/* Info overlay on image (name + price) */
.overlayName {
position: absolute;
top: 12px;
left: 12px;
z-index: 2;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 700;
font-size: 15px;
line-height: 1.3;
color: #fff;
mix-blend-mode: difference;
margin: 0;
max-width: 70%;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 2;
pointer-events: none;
text-transform: capitalize;
}
@media (min-width: 600px) {
.overlayName {
font-size: 16px;
}
}
.overlayPrice {
display: flex;
align-items: center;
gap: 8px;
}
.overlayNow {
color: #fff;
font-weight: 800;
font-size: 13px;
}
.overlayOld {
color: rgba(255,255,255,0.8);
font-size: 12px;
text-decoration: line-through;
}
/* Bottom overlay bar: price left, qty right */
.bottomOverlay {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: rgba(0,0,0,0.6);
color: #fff;
border-radius: 0 0 12px 12px;
}
/* New stacked price for clarity */
.overlayPriceCol {
display: flex;
flex-direction: column;
align-items: flex-start;
line-height: 1.2;
}
.overlayOriginal {
color: rgba(255,255,255,0.85);
font-weight: 700;
font-size: 12px;
text-decoration: line-through;
}
@media (min-width: 600px) {
.overlayOriginal {
font-size: 13px;
}
}
.overlayPromo {
color: #fff;
font-weight: 800;
font-size: 17px;
}
@media (min-width: 600px) {
.overlayOriginal {
font-size: 15px;
}
.overlayPromo {
font-size: 19px;
}
}
/* Title text below image for portrait mode */
.title {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 700;
font-size: 16px;
color: #333;
margin: 10px 4px 6px 4px;
text-transform: capitalize;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 2;
}
@media (min-width: 600px) {
.title {
font-size: 17px;
}
}
.itemInvoice .itemDetails {
@@ -233,7 +630,7 @@
.itemInvoice .itemName,
.itemInvoice .itemPrice,
.itemInvoice .itemQty .qtyInvoice .multiplySymbol {
font-size: 0.9rem;
font-size: 1rem;
}
.blank {
@@ -246,6 +643,98 @@
.createItem {
position: absolute;
left: 15px;
right: 15px;
left: 20px;
right: 20px;
bottom: 20px;
}
/* Item list container */
.item-list-container {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px 0;
}
/* Section header */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #e6e6e6;
margin-bottom: 16px;
}
.section-title {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 700;
font-size: 20px;
color: #333;
margin: 0;
}
.section-actions {
display: flex;
gap: 12px;
}
/* Empty state */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
text-align: center;
background: #f8f9fa;
border-radius: 12px;
margin: 20px 0;
}
.empty-state-icon {
font-size: 48px;
color: #ccc;
margin-bottom: 16px;
}
.empty-state-title {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;
font-size: 18px;
color: #666;
margin-bottom: 8px;
}
.empty-state-description {
font-family: "Plus Jakarta Sans", sans-serif;
font-size: 14px;
color: #999;
margin-bottom: 20px;
max-width: 300px;
}
/* Card variant */
.item-card {
background: #ffffff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
padding: 16px;
transition: all 0.2s ease;
}
.item-card:hover { /* no hover */ }
/* List variant */
.item-list-item {
background: #ffffff;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
padding: 16px;
display: flex;
align-items: center;
gap: 16px;
transition: all 0.2s ease;
}
.item-list-item:hover { /* no hover */ }

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import styles from "./Modal.module.css";
import { getImageUrl } from "../helpers/itemHelper.js";
@@ -23,9 +24,10 @@ const ItemConfig = ({
const [itemDescription, setItemDescription] = useState(initialDescription);
const fileInputRef = useRef(null);
const textareaRef = useRef(null);
const [saving, setSaving] = useState(false);
const [saveStatus, setSaveStatus] = useState(null); // 'success' | 'error'
useEffect(() => {
// Prevent scrolling when modal is open
document.body.style.overflow = "hidden";
if(selectedImage){
@@ -80,139 +82,135 @@ const ItemConfig = ({
}
}, [textareaRef.current]);
const handleCreate = () => {
console.log(itemPromoPrice)
handleCreateItem(itemName, itemPrice, selectedImage, previewUrl, itemDescription, itemPromoPrice);
document.body.style.overflow = "auto";
const handleCreate = async () => {
setSaving(true);
setSaveStatus(null);
try {
await Promise.resolve(handleCreateItem(itemName, itemPrice, selectedImage, previewUrl, itemDescription, itemPromoPrice));
setSaveStatus('success');
} catch (e) {
setSaveStatus('error');
} finally {
setSaving(false);
}
};
const handleUpdate = () => {
console.log(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice)
handleUpdateItem(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice);
document.body.style.overflow = "auto";
const handleUpdate = async () => {
setSaving(true);
setSaveStatus(null);
try {
await Promise.resolve(handleUpdateItem(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice));
setSaveStatus('success');
} catch (e) {
setSaveStatus('error');
} finally {
setSaving(false);
}
};
return (
<div onClick={handleOverlayClick} style={{ position: 'fixed', width: '100vw', height: '100vh', left: 0, bottom: 0, display: 'flex', flexDirection: 'column-reverse', zIndex: 301, backgroundColor: '#00000061' }}>
<div onClick={handleContentClick} style={{ display: 'flex', flexDirection: 'column', padding: '15px', backgroundColor: 'white', borderRadius: '20px 20px 0 0', overflowY: 'auto' }}>
<div style={{ display: 'flex' }}>
<div style={{ width: '26vw', height: '26vw', marginRight: '10px' }}>
<img style={{ width: '100%', height: '100%', borderRadius: '10px', objectFit: 'cover' }} src={previewUrl} />
<input
type="file"
ref={fileInputRef}
className={styles.fileInput}
onChange={handleFileChange}
accept="image/*"
style={{ display: "none" }}
/>
return createPortal(
<div onClick={handleOverlayClick} className={styles.modalOverlay}>
<div onClick={handleContentClick} className={styles.modalContent}>
<div className={styles.imageSection}>
<div className={styles.imagePreview}>
<img src={previewUrl} alt="Preview" className={styles.previewImage} />
</div>
<div style={{ width: '72%', height: '26vw', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<div onClick={() => handleChangeImage()} style={{ width: '140px', marginRight: '10px', height: '40px', alignContent: 'center', textAlign: 'center', borderRadius: '10px', border: '1px solid #60d37e', color: '#60d37e', backgroundColor: 'white', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
{isBeingEdit ? 'Ganti gambar' : 'Tambah gambar'}
</div>
<div onClick={handleDelete} style={{ width: '76px', height: '40px', alignContent: 'center', textAlign: 'center', borderRadius: '10px', border: '1px solid rgb(211 96 96)', color: 'rgb(211 96 96)', backgroundColor: 'white', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
Hapus
</div>
<div className={styles.imageActions}>
<button
onClick={handleChangeImage}
className={styles.actionButton}
>
{isBeingEdit ? 'Ganti Gambar' : 'Tambah Gambar'}
</button>
{isBeingEdit && (
<button
onClick={handleDelete}
className={`${styles.actionButton} ${styles.deleteButton}`}
>
Hapus
</button>
)}
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', color: 'black', marginTop: '10px' }}>
<p style={{ marginBottom: '5px', fontWeight: '500' }}>Nama item</p>
<input
value={itemName}
style={{
padding: '10px',
borderRadius: '8px',
border: '1px solid #ccc',
fontSize: '14px',
width: '100%',
marginBottom: '15px',
outline: 'none',
transition: 'all 0.3s ease',
boxSizing: 'border-box', // Make sure the padding doesn't cause overflow
}}
onChange={(e)=>setItemName(e.target.value)}
onFocus={(e) => e.target.style.borderColor = '#60d37e'}
onBlur={(e) => e.target.style.borderColor = '#ccc'}
type="file"
ref={fileInputRef}
className={styles.fileInput}
onChange={handleFileChange}
accept="image/*"
style={{ display: "none" }}
/>
</div>
<div style={{ display: 'flex', color: 'black', justifyContent: 'space-between' }}>
<div style={{ width: '48%' }}>
<p style={{ marginBottom: '5px', fontWeight: '500', marginTop: 0 }}>Harga</p>
<div className={styles.formSection}>
<div className={styles.bannerRow}>
{saveStatus === 'success' && (
<span className={`${styles.banner} ${styles.bannerSuccess}`}>Perubahan disimpan</span>
)}
{saveStatus === 'error' && (
<span className={`${styles.banner} ${styles.bannerError}`}>Gagal menyimpan perubahan</span>
)}
</div>
<div className={styles.formGroup}>
<label className={styles.formLabel}>Nama Item</label>
<input
value={itemPrice}
style={{
padding: '10px',
borderRadius: '8px',
border: '1px solid #ccc',
fontSize: '14px',
width: '100%',
marginBottom: '15px',
outline: 'none',
transition: 'all 0.3s ease',
boxSizing: 'border-box',
}}
onChange={(e)=>setItemPrice(e.target.value)}
onFocus={(e) => e.target.style.borderColor = '#60d37e'}
onBlur={(e) => e.target.style.borderColor = '#ccc'}
value={itemName}
className={styles.formInput}
onChange={(e)=>setItemName(e.target.value)}
placeholder="Masukkan nama item"
/>
</div>
<div style={{ width: '48%' }}>
<p style={{ marginBottom: '5px', fontWeight: '500', marginTop: 0 }}>Harga promo</p>
<input
value={itemPromoPrice}
placeholder="Opsional"
style={{
padding: '10px',
borderRadius: '8px',
border: '1px solid #ccc',
fontSize: '14px',
width: '100%',
marginBottom: '15px',
outline: 'none',
transition: 'all 0.3s ease',
boxSizing: 'border-box',
}}
onChange={(e)=>setItemPromoPrice(e.target.value)}
onFocus={(e) => e.target.style.borderColor = '#60d37e'}
onBlur={(e) => e.target.style.borderColor = '#ccc'}
<div className={styles.formRow}>
<div className={styles.formGroup}>
<label className={styles.formLabel}>Harga</label>
<input
value={itemPrice}
className={styles.formInput}
onChange={(e)=>setItemPrice(e.target.value)}
placeholder="Rp 0"
/>
</div>
<div className={styles.formGroup}>
<label className={styles.formLabel}>Harga Promo</label>
<input
value={itemPromoPrice}
className={styles.formInput}
onChange={(e)=>setItemPromoPrice(e.target.value)}
placeholder="Opsional"
/>
</div>
</div>
<div className={styles.formGroup}>
<label className={styles.formLabel}>Deskripsi</label>
<textarea
ref={textareaRef}
className={styles.formTextarea}
placeholder="Tambahkan deskripsi item..."
value={itemDescription}
onChange={(e)=>setItemDescription(e.target.value)}
/>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', color: 'black' }}>
<p style={{ marginBottom: '5px', fontWeight: '500', marginTop: 0 }}>Deskripsi</p>
<textarea
ref={textareaRef}
style={{
padding: '10px',
borderRadius: '8px',
border: '1px solid #ccc',
fontSize: '14px',
width: '100%',
marginBottom: '15px',
outline: 'none',
transition: 'all 0.3s ease',
boxSizing: 'border-box',
resize: 'none', // Prevent manual resize that could cause overflow
}}
placeholder="Tambah deskripsi..."
value={itemDescription}
onChange={(e)=>setItemDescription(e.target.value)}
onFocus={(e) => e.target.style.borderColor = '#60d37e'}
onBlur={(e) => e.target.style.borderColor = '#ccc'}
/>
</div>
<div style={{ width: '100%', height: '35px', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<div onClick={() => {isBeingEdit ? handleUpdate() : handleCreate()} } style={{ width: '100%', height: '40px', alignContent: 'center', textAlign: 'center', borderRadius: '10px', border: '1px solid #60d37e', color: '#60d37e', backgroundColor: 'white', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
{isBeingEdit? 'Simpan' : 'Buat Item'}
<div className={styles.formActions}>
<button
onClick={cancelEdit}
className={`${styles.formButton} ${styles.cancelButton}`}
disabled={saving}
>
Batal
</button>
<button
onClick={() => { isBeingEdit ? handleUpdate() : handleCreate() }}
className={`${styles.formButton} ${styles.saveButton}`}
disabled={saving}
>
{saving ? 'Menyimpan…' : (isBeingEdit ? 'Simpan Perubahan' : 'Buat Item')}
</button>
</div>
</div>
</div>
</div>
</div>,
document.body
);
};

View File

@@ -8,7 +8,7 @@
bottom: 0;
display: flex;
flex-direction: column-reverse;
z-index: 200;
z-index: 2147483600 !important; /* above watermark and page layers */
background-color: rgba(0, 0, 0, 0.38); /* #00000061 */
}

View File

@@ -17,6 +17,7 @@ import {
import ItemType from "./ItemType.js";
import { createItemType, updateItemDeletionStatus } from "../helpers/itemHelper.js";
import ItemConfig from "./ItemConfig.js"
import { ArrowUp, ArrowDown, Pencil, Save, X } from 'lucide-react';
const ItemLister = ({
index,
@@ -73,6 +74,22 @@ const ItemLister = ({
const [randomKey, setRandomKey] = useState(0);
// Dummy items for style preview on cafe page (non-invoice, non-edit)
const showGrid = !isEdit && !forInvoice;
const dummyCount = showGrid ? Math.max(0, 6 - items.length) : 0;
const dummyItems = Array.from({ length: dummyCount }, (_, i) => ({
itemId: `dummy-${itemTypeId}-${i}`,
name: `Sample ${i + 1}`,
price: 25000 + i * 5000,
promoPrice: i % 2 === 1 ? 20000 + i * 3000 : 0,
qty: 0,
description: 'Contoh deskripsi singkat item.',
image: getImageUrl(`uploads/samples/sample (${(i % 16) + 1}).png`),
availability: true,
selectedImage: null,
}));
const displayItems = items; // no dummy items on cafe page
const handlePlusClick = (itemId) => {
const updatedItems = items.map((item) => {
if (item.itemId === itemId) {
@@ -598,7 +615,7 @@ const ItemLister = ({
return (
<>
{(items.length > 0 ||
(user && (user.cafeId == shopId || user.userId == shopOwnerId))) && (
(user && (user.cafeId == shopId || user.user_id == shopOwnerId))) && (
<div
key={itemTypeId}
className={`${styles["item-lister"]} ${isEdit ? styles["fullscreen"] : ""
@@ -608,7 +625,15 @@ const ItemLister = ({
{(isEdit && isFirstStep || !isEdit) &&
<div className={styles["title-container"]}>
{isEdit && <ItemType blank={true} imageUrl={previewUrl} />}
{isEdit && (
<div className={styles["image-preview"]}>
<img
src={previewUrl}
alt="Category Preview"
className={styles["preview-image"]}
/>
</div>
)}
<input
className={`${styles.title} ${isEdit ? styles.border : styles.noborder
}`}
@@ -618,87 +643,49 @@ const ItemLister = ({
disabled={!isEdit}
/>
{isEditMode && !isEdit && (
<>
<div
style={{
width: '32px',
height: '32px', // Add a height to the div
display: 'flex', // Use flexbox
justifyContent: 'center', // Center horizontally
alignItems: 'center', // Center vertically
cursor: 'pointer'
}}
onClick={index == 0 ? null : () => moveItemTypeUp(itemTypeId)} // Move onClick here for the whole div
<div className={styles.titleActions}>
<button
className={styles.iconBtn}
onClick={() => index === 0 ? null : moveItemTypeUp(itemTypeId)}
disabled={index === 0}
aria-label="Naikkan kategori"
>
<svg
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
fill="#000000"
style={{ width: '100%', height: '100%' }} // Ensure SVG fits the div
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path d="m 1 11 c 0 -0.265625 0.105469 -0.519531 0.292969 -0.707031 l 6 -6 c 0.390625 -0.390625 1.023437 -0.390625 1.414062 0 l 6 6 c 0.1875 0.1875 0.292969 0.441406 0.292969 0.707031 s -0.105469 0.519531 -0.292969 0.707031 c -0.390625 0.390625 -1.023437 0.390625 -1.414062 0 l -5.292969 -5.292969 l -5.292969 5.292969 c -0.390625 0.390625 -1.023437 0.390625 -1.414062 0 c -0.1875 -0.1875 -0.292969 -0.441406 -0.292969 -0.707031 z m 0 0" fill={index === 0 ? "gray" : "#2e3436"}></path>
</g>
</svg>
</div>
<div
style={{
width: '32px',
height: '32px', // Add a height to the div
display: 'flex', // Use flexbox
justifyContent: 'center', // Center horizontally
alignItems: 'center', // Center vertically
cursor: 'pointer'
}}
onClick={index == indexTotal - 1 ? null : () => moveItemTypeDown(itemTypeId)} // Move onClick here for the whole div
<ArrowUp size={20} />
</button>
<button
className={styles.iconBtn}
onClick={() => index === indexTotal - 1 ? null : moveItemTypeDown(itemTypeId)}
disabled={index === indexTotal - 1}
aria-label="Turunkan kategori"
>
<svg
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
fill="#000000"
style={{ width: '100%', height: '100%' }} // Ensure SVG fits the div
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path d="m 1 5 c 0 -0.265625 0.105469 -0.519531 0.292969 -0.707031 c 0.390625 -0.390625 1.023437 -0.390625 1.414062 0 l 5.292969 5.292969 l 5.292969 -5.292969 c 0.390625 -0.390625 1.023437 -0.390625 1.414062 0 c 0.1875 0.1875 0.292969 0.441406 0.292969 0.707031 s -0.105469 0.519531 -0.292969 0.707031 l -6 6 c -0.390625 0.390625 -1.023437 0.390625 -1.414062 0 l -6 -6 c -0.1875 -0.1875 -0.292969 -0.441406 -0.292969 -0.707031 z m 0 0" fill={index === indexTotal - 1 ? "gray" : "#2e3436"}></path>
</g>
</svg>
</div>
<div
style={{
width: '32px',
height: '32px', // Add a height to the div
display: 'flex', // Use flexbox
justifyContent: 'center', // Center horizontally
alignItems: 'center', // Center vertically
cursor: 'pointer'
}}
onClick={toggleEditTypeItem} // Move onClick here for the whole div
<ArrowDown size={20} />
</button>
<button
className={styles.iconBtn}
onClick={toggleEditTypeItem}
aria-label="Edit kategori"
>
<svg
fill="#000000"
viewBox="0 0 32 32"
style={{ fillRule: 'evenodd', clipRule: 'evenodd', strokeLinejoin: 'round', strokeMiterlimit: 2 }}
version="1.1"
xmlSpace="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlnsSerif="http://www.serif.com/"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path d="M12.965,5.462c0,-0 -2.584,0.004 -4.979,0.008c-3.034,0.006 -5.49,2.467 -5.49,5.5l0,13.03c0,1.459 0.579,2.858 1.611,3.889c1.031,1.032 2.43,1.611 3.889,1.611l13.003,0c3.038,-0 5.5,-2.462 5.5,-5.5c0,-2.405 0,-5.004 0,-5.004c0,-0.828 -0.672,-1.5 -1.5,-1.5c-0.827,-0 -1.5,0.672 -1.5,1.5l0,5.004c0,1.381 -1.119,2.5 -2.5,2.5l-13.003,0c-0.663,-0 -1.299,-0.263 -1.768,-0.732c-0.469,-0.469 -0.732,-1.105 -0.732,-1.768l0,-13.03c0,-1.379 1.117,-2.497 2.496,-2.5c2.394,-0.004 4.979,-0.008 4.979,-0.008c0.828,-0.002 1.498,-0.675 1.497,-1.503c-0.001,-0.828 -0.675,-1.499 -1.503,-1.497Z"></path>
<path d="M20.046,6.411l-6.845,6.846c-0.137,0.137 -0.232,0.311 -0.271,0.501l-1.081,5.152c-0.069,0.329 0.032,0.671 0.268,0.909c0.237,0.239 0.577,0.343 0.907,0.277l5.194,-1.038c0.193,-0.039 0.371,-0.134 0.511,-0.274l6.845,-6.845l-5.528,-5.528Zm1.415,-1.414l5.527,5.528l1.112,-1.111c1.526,-1.527 1.526,-4.001 -0,-5.527c-0.001,-0 -0.001,-0.001 -0.001,-0.001c-1.527,-1.526 -4.001,-1.526 -5.527,-0l-1.111,1.111Z"></path>
</g>
</svg>
</div>
</>
<Pencil size={20} />
</button>
</div>
)}
{isEditMode && isEdit && (
<div className={styles.titleActions}>
<button
className={styles.iconBtn}
onClick={handleSaveType}
aria-label="Simpan"
>
<Save size={20} />
</button>
<button
className={styles.iconBtn}
onClick={resetItems}
aria-label="Batal"
>
<X size={20} />
</button>
</div>
)}
</div>
}
@@ -712,6 +699,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage)
}
imageUrl={getImageUrl("uploads/assets/addnew.png")}
compact={false}
/>
{/* {typeImage != null && !previewUrl.includes(typeImage) && (
<ItemType
@@ -729,6 +717,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage)
}
imageUrl={getImageUrl("uploads/assets/beverage4.jpg")}
compact={false}
/>
<ItemType
rectangular={true}
@@ -736,6 +725,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage)
}
imageUrl={getImageUrl("uploads/assets/beverage1.png")}
compact={false}
/>
<ItemType
rectangular={true}
@@ -743,6 +733,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage)
}
imageUrl={getImageUrl("uploads/assets/beverage2.png")}
compact={false}
/>
<ItemType
rectangular={true}
@@ -750,6 +741,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage)
}
imageUrl={getImageUrl("uploads/assets/beverage3.png")}
compact={false}
/>
<ItemType
rectangular={true}
@@ -757,6 +749,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage)
}
imageUrl={getImageUrl("uploads/assets/snack5.png")}
compact={false}
/>
<ItemType
rectangular={true}
@@ -764,6 +757,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage)
}
imageUrl={getImageUrl("uploads/assets/dessert1.png")}
compact={false}
/>
<ItemType
rectangular={true}
@@ -771,6 +765,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage)
}
imageUrl={getImageUrl("uploads/assets/dessert2.jpg")}
compact={false}
/>
<ItemType
rectangular={true}
@@ -778,6 +773,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage)
}
imageUrl={getImageUrl("uploads/assets/food4.jpg")}
compact={false}
/>
<ItemType
rectangular={true}
@@ -785,6 +781,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage)
}
imageUrl={getImageUrl("uploads/assets/food1.png")}
compact={false}
/>
<ItemType
rectangular={true}
@@ -792,6 +789,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage)
}
imageUrl={getImageUrl("uploads/assets/food2.jpg")}
compact={false}
/>
<ItemType
@@ -800,6 +798,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage)
}
imageUrl={getImageUrl("uploads/assets/food3.png")}
compact={false}
/>
<ItemType
rectangular={true}
@@ -807,6 +806,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage)
}
imageUrl={getImageUrl("uploads/assets/packet1.png")}
compact={false}
/>
<ItemType
rectangular={true}
@@ -814,6 +814,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage)
}
imageUrl={getImageUrl("uploads/assets/packet2.png")}
compact={false}
/>
<ItemType
rectangular={true}
@@ -821,6 +822,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage)
}
imageUrl={getImageUrl("uploads/assets/snack1.png")}
compact={false}
/>
<ItemType
rectangular={true}
@@ -828,6 +830,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage)
}
imageUrl={getImageUrl("uploads/assets/snack2.png")}
compact={false}
/>
<ItemType
rectangular={true}
@@ -835,6 +838,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage)
}
imageUrl={getImageUrl("uploads/assets/snack3.png")}
compact={false}
/>
<ItemType
rectangular={true}
@@ -842,6 +846,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage)
}
imageUrl={getImageUrl("uploads/assets/snack4.png")}
compact={false}
/>
{Array.from({ length: 16 }, (_, index) => {
const sampleNumber = index + 1; // To get numbers from 1 to 16
@@ -853,33 +858,53 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage)
}
imageUrl={getImageUrl(`uploads/samples/sample (${sampleNumber}).png`)}
compact={false}
/>
);
})}
</div>
<button onClick={() => setIsFirstStep(false)} style={{ width: '100%', height: '40px', borderRadius: '20px' }}>selanjutnya</button>
<button onClick={() => setIsFirstStep(false)} className={styles["add-item-button"]}>
Selanjutnya
</button>
</>
)}
{(isEdit && !isFirstStep || !isEdit) &&
<div key={randomKey}>
{isEdit && <div style={{ display: 'flex', justifyContent: 'flex-start' }}><div style={{ marginTop: '49px', marginRight: '10px', marginLeft: '10px' }} onClick={() => setIsFirstStep(true)}><svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 512 512"><path d="M48,256c0,114.87,93.13,208,208,208s208-93.13,208-208S370.87,48,256,48,48,141.13,48,256Zm212.65-91.36a16,16,0,0,1,.09,22.63L208.42,240H342a16,16,0,0,1,0,32H208.42l52.32,52.73A16,16,0,1,1,238,347.27l-79.39-80a16,16,0,0,1,0-22.54l79.39-80A16,16,0,0,1,260.65,164.64Z" /></svg></div>
<h2 className={styles["item-list-title"]}>{items && items.length < 1 ? 'Buat item' : 'Daftar item'}</h2></div>}
<div className={styles["item-list"]}>
{isEdit && (
<div className={styles["settings-section"]}>
<h3 className={styles["settings-title"]}>Pengaturan Kategori</h3>
<div className={styles["setting-row"]}>
<span className={styles["setting-label"]}>Visibilitas Kategori</span>
<div className={styles["switch-container"]}>
<span className={styles["switch-label"]}>
{isVisible ? "Tampil" : "Tersembunyi"}
</span>
<Switch
onChange={() => setIsVisible(!isVisible)}
checked={isVisible}
offColor="#cccccc"
onColor="#6B8F71"
uncheckedIcon={false}
checkedIcon={false}
height={24}
width={48}
handleDiameter={20}
/>
</div>
</div>
</div>
)}
<div className={`${styles["item-list"]} ${(!isEdit && !forInvoice) ? styles["item-grid"] : ""}`}>
{user && (
user.userId == shopOwnerId || user.cafeId == shopId) &&
user.user_id == shopOwnerId || user.cafeId == shopId) &&
isEditMode && (
<>
{!isAddingNewItem && (
<button
className={styles["add-item-button"]}
onClick={toggleAddNewItem}
style={{
display: "inline-block",
height: "120px",
fontSize: "20px",
}}
>
Tambah item +
+ Tambah Item Baru
</button>
)}
{isAddingNewItem && (
@@ -888,7 +913,7 @@ const ItemLister = ({
cancelEdit={() => toggleAddNewItem()}
handleCreateItem={onCreateItem}
/>
<Item blank={true} handleCreateItem={onCreateItem} />
<Item blank={true} handleCreateItem={onCreateItem} hideDetails={!showGrid} />
</>
)}
</>
@@ -915,48 +940,29 @@ const ItemLister = ({
{isEditMode && isEditItem != item.itemId && (
<div className={styles["editModeLayout"]}>
<div style={{ display: 'flex', alignItems: 'center', height: '40px', marginLeft: '7.5vw' }}>
<div>
{isEditMode && (
<Switch
onChange={() => handleChange(item.itemId)}
checked={item.availability}
/>
<div className={styles["switch-container"]}>
<span className={styles["switch-label"]}>
{item.availability ? "Tersedia" : "Tidak Tersedia"}
</span>
<Switch
onChange={() => handleChange(item.itemId)}
checked={item.availability}
offColor="#cccccc"
onColor="#6B8F71"
uncheckedIcon={false}
checkedIcon={false}
height={20}
width={40}
handleDiameter={16}
/>
</div>
)}
<h3>
&nbsp;{item.availability ? "Tersedia" : "Tidak tersedia"}
</h3>
</div>
<div onClick={() => editItem(item.itemId)} style={{ display: 'flex', alignItems: 'center', height: '40px', marginRight: '7.5vw' }}>
<div
style={{
width: '32px',
height: '32px', // Add a height to the div
display: 'flex', // Use flexbox
justifyContent: 'center', // Center horizontally
alignItems: 'center', // Center vertically
cursor: 'pointer'
}}
>
<svg
fill="white"
viewBox="0 0 32 32"
style={{ fillRule: 'evenodd', clipRule: 'evenodd', strokeLinejoin: 'round', strokeMiterlimit: 2 }}
version="1.1"
xmlSpace="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlnsSerif="http://www.serif.com/"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path d="M12.965,5.462c0,-0 -2.584,0.004 -4.979,0.008c-3.034,0.006 -5.49,2.467 -5.49,5.5l0,13.03c0,1.459 0.579,2.858 1.611,3.889c1.031,1.032 2.43,1.611 3.889,1.611l13.003,0c3.038,-0 5.5,-2.462 5.5,-5.5c0,-2.405 0,-5.004 0,-5.004c0,-0.828 -0.672,-1.5 -1.5,-1.5c-0.827,-0 -1.5,0.672 -1.5,1.5l0,5.004c0,1.381 -1.119,2.5 -2.5,2.5l-13.003,0c-0.663,-0 -1.299,-0.263 -1.768,-0.732c-0.469,-0.469 -0.732,-1.105 -0.732,-1.768l0,-13.03c0,-1.379 1.117,-2.497 2.496,-2.5c2.394,-0.004 4.979,-0.008 4.979,-0.008c0.828,-0.002 1.498,-0.675 1.497,-1.503c-0.001,-0.828 -0.675,-1.499 -1.503,-1.497Z"></path>
<path d="M20.046,6.411l-6.845,6.846c-0.137,0.137 -0.232,0.311 -0.271,0.501l-1.081,5.152c-0.069,0.329 0.032,0.671 0.268,0.909c0.237,0.239 0.577,0.343 0.907,0.277l5.194,-1.038c0.193,-0.039 0.371,-0.134 0.511,-0.274l6.845,-6.845l-5.528,-5.528Zm1.415,-1.414l5.527,5.528l1.112,-1.111c1.526,-1.527 1.526,-4.001 -0,-5.527c-0.001,-0 -0.001,-0.001 -0.001,-0.001c-1.527,-1.526 -4.001,-1.526 -5.527,-0l-1.111,1.111Z"></path>
</g>
</svg>
</div>
<h3>Edit item</h3>
<div onClick={() => editItem(item.itemId)}>
<Pencil size={18} />
</div>
</div>
)}
@@ -971,6 +977,7 @@ const ItemLister = ({
qty={item.qty}
imageUrl={item.image}
imageFile={item.selectedImage}
hideDetails={!showGrid}
onPlusClick={() => handlePlusClick(item.itemId)}
onNegativeClick={() => handleNegativeClick(item.itemId)}
onRemoveClick={() => handleRemoveClick(item.itemId)}
@@ -990,13 +997,6 @@ const ItemLister = ({
<div
key={item.itemId}>
{isEditItem == item.itemId && (
// <button
// className={styles["add-item-button"]}
// onClick={() => editItem(0)}
// style={{ display: "inline-block" }}
// >
// batal
// </button>
<ItemConfig
isBeingEdit={true}
name={item.name}
@@ -1022,23 +1022,30 @@ const ItemLister = ({
<div className={styles["itemWrapper"]}>
{(isEditMode && isEditItem != item.itemId || item.willBeDeleted) && (
<div className={styles["editModeLayout"]}>
<div style={{ display: 'flex', alignItems: 'center', height: '40px', marginLeft: '7.5vw' }}>
<div>
{!item.willBeDeleted && isEditMode && (
<Switch
onChange={() => handleChange(item.itemId)}
checked={item.availability}
/>
<div className={styles["switch-container"]}>
<span className={styles["switch-label"]}>
{item.availability ? "Tersedia" : "Tidak Tersedia"}
</span>
<Switch
onChange={() => handleChange(item.itemId)}
checked={item.availability}
offColor="#cccccc"
onColor="#6B8F71"
uncheckedIcon={false}
checkedIcon={false}
height={20}
width={40}
handleDiameter={16}
/>
</div>
)}
{item.willBeDeleted ?
<h3 style={{ backgroundColor: 'black', padding: '13px 26px' }}>
{item.willBeDeleted && (
<span style={{ backgroundColor: 'black', padding: '6px 12px', borderRadius: '20px' }}>
Ditandai untuk dihapus
</h3>
:
<h3>
&nbsp;{item.availability ? "Tersedia" : "Tidak tersedia"}
</h3>
}
</span>
)}
</div>
<div onClick={() => {
@@ -1047,38 +1054,8 @@ const ItemLister = ({
} else {
handleItemDeletionToggle(item.itemId, false);
}
}}
style={{ display: 'flex', alignItems: 'center', height: '40px', marginRight: '7.5vw' }}>
{!item.willBeDeleted && <div
style={{
width: '32px',
height: '32px', // Add a height to the div
display: 'flex', // Use flexbox
justifyContent: 'center', // Center horizontally
alignItems: 'center', // Center vertically
cursor: 'pointer'
}}
>
<svg
fill="white"
viewBox="0 0 32 32"
style={{ fillRule: 'evenodd', clipRule: 'evenodd', strokeLinejoin: 'round', strokeMiterlimit: 2 }}
version="1.1"
xmlSpace="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlnsSerif="http://www.serif.com/"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path d="M12.965,5.462c0,-0 -2.584,0.004 -4.979,0.008c-3.034,0.006 -5.49,2.467 -5.49,5.5l0,13.03c0,1.459 0.579,2.858 1.611,3.889c1.031,1.032 2.43,1.611 3.889,1.611l13.003,0c3.038,-0 5.5,-2.462 5.5,-5.5c0,-2.405 0,-5.004 0,-5.004c0,-0.828 -0.672,-1.5 -1.5,-1.5c-0.827,-0 -1.5,0.672 -1.5,1.5l0,5.004c0,1.381 -1.119,2.5 -2.5,2.5l-13.003,0c-0.663,-0 -1.299,-0.263 -1.768,-0.732c-0.469,-0.469 -0.732,-1.105 -0.732,-1.768l0,-13.03c0,-1.379 1.117,-2.497 2.496,-2.5c2.394,-0.004 4.979,-0.008 4.979,-0.008c0.828,-0.002 1.498,-0.675 1.497,-1.503c-0.001,-0.828 -0.675,-1.499 -1.503,-1.497Z"></path>
<path d="M20.046,6.411l-6.845,6.846c-0.137,0.137 -0.232,0.311 -0.271,0.501l-1.081,5.152c-0.069,0.329 0.032,0.671 0.268,0.909c0.237,0.239 0.577,0.343 0.907,0.277l5.194,-1.038c0.193,-0.039 0.371,-0.134 0.511,-0.274l6.845,-6.845l-5.528,-5.528Zm1.415,-1.414l5.527,5.528l1.112,-1.111c1.526,-1.527 1.526,-4.001 -0,-5.527c-0.001,-0 -0.001,-0.001 -0.001,-0.001c-1.527,-1.526 -4.001,-1.526 -5.527,-0l-1.111,1.111Z"></path>
</g>
</svg>
</div>
}
<h3>{item.willBeDeleted ? 'Batalkan' : 'Edit item'}</h3>
}}>
<Pencil size={18} />
</div>
</div>
)}
@@ -1088,6 +1065,8 @@ const ItemLister = ({
last={index === indexTotal - 1 && indexx === items.length - 1}
forCart={forCart}
forInvoice={forInvoice}
portrait={showGrid}
hideDetails={!showGrid}
name={item.name}
price={item.price}
promoPrice={item.promoPrice}
@@ -1097,9 +1076,9 @@ const ItemLister = ({
itemTypeId ? getImageUrl(item.image) : item.image
}
imageFile={item.selectedImage}
onPlusClick={() => handlePlusClick(item.itemId)}
onNegativeClick={() => handleNegativeClick(item.itemId)}
onRemoveClick={() => handleRemoveClick(item.itemId)}
onPlusClick={() => (String(item.itemId).startsWith('dummy-') ? undefined : handlePlusClick(item.itemId))}
onNegativeClick={() => (String(item.itemId).startsWith('dummy-') ? undefined : handleNegativeClick(item.itemId))}
onRemoveClick={() => (String(item.itemId).startsWith('dummy-') ? undefined : handleRemoveClick(item.itemId))}
isBeingEdit={isEditItem == item.itemId}
isAvailable={item.availability}
handleUpdateItem={(name, price, image, description, promoPrice) =>
@@ -1113,7 +1092,7 @@ const ItemLister = ({
{user &&
user.roleId == 1 &&
user.userId == shopOwnerId &&
user.user_id == shopOwnerId &&
isEdit && (
<>
{/* <button
@@ -1128,33 +1107,6 @@ const ItemLister = ({
</div>
</div>
}
{isEdit && (
<div className={styles.PaymentOption}>
<div className={styles.TotalContainer}>
<span>Pengaturan</span>
<span></span>
</div>
<div className={styles.OptionContainer}>
<span>sembunyikan semua</span>
<span>
<Switch
onChange={() => setIsVisible(!isVisible)}
checked={!isVisible}
/>
</span>
</div>
<button onClick={handleSaveType} className={styles.PayButton}>
{false ? (
<ColorRing height="50" width="50" color="white" />
) : (
"Simpan"
)}
</button>
<div className={styles.Pay2Button} onClick={resetItems}>
Kembali
</div>
</div>
)}
</div>
)}
</>

View File

@@ -2,80 +2,88 @@
.item-lister {
width: 100%;
padding: 10px; /* Adjust padding as needed */
box-sizing: border-box; /* Ensure padding doesn't affect width */
padding: 16px;
box-sizing: border-box;
white-space: break-spaces;
background-color: #ffffff;
border-radius: 16px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
margin-bottom: 20px;
position: relative;
z-index: 0 !important; /* ensure behind modal overlay */
}
.fullscreen {
position: fixed; /* Keep the container fixed */
top: 0; /* Adjust the top position as needed */
bottom: 0; /* Occupy the full height of the viewport */
left: 0; /* Align to the left */
right: 0; /* Align to the right */
background-color: white; /* Background color */
z-index: 1000; /* Layering */
overflow-y: auto; /* Allow vertical scrolling */
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: white;
z-index: 100; /* keep above page, below modal overlays (>=200) */
overflow-y: auto;
}
.grid-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
/* padding: 10px; */
/* max-height: calc(3 * (25vw - 20px) + 20px); */
overflow-y: auto;
height: calc(49vw - 20px);
gap: 16px;
height: calc(52vw - 20px);
padding: 16px;
background-color: #f8f9fa;
border-radius: 12px;
margin-bottom: 24px;
}
@media (min-height: 0px) {
.grid-container {
height: 27vh;
height: 30vh;
}
}
@media (min-height: 630px) {
.grid-container {
height: 27vh;
height: 30vh;
}
}
@media (min-height: 636px) {
.grid-container {
height: 29vh;
height: 32vh;
}
}
@media (min-height: 650px) {
.grid-container {
height: 34vh;
height: 38vh;
}
}
@media (min-height: 705px) {
.grid-container {
height: 37vh;
height: 41vh;
}
}
@media (min-height: 735px) {
.grid-container {
height: 38vh;
height: 42vh;
}
}
@media (min-height: 759px) {
.grid-container {
height: 40vh;
height: 44vh;
}
}
@media (min-height: 819px) {
.grid-container {
height: 44vh;
height: 48vh;
}
}
@media (min-height: 830px) {
.grid-container {
height: 47vh;
height: 51vh;
}
}
@media (min-height: 892px) {
.grid-container {
height: 49vh;
height: 53vh;
}
}
@@ -83,46 +91,166 @@
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
background-color: #f8f9fa;
border-radius: 12px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
position: relative;
z-index: 0 !important; /* ensure behind modal overlay */
}
.titleActions {
display: inline-flex;
align-items: center;
gap: 10px;
}
.iconBtn {
width: 40px;
height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid #e6e6e6;
background: #ffffff;
color: #2d2d2d;
border-radius: 10px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
position: relative;
z-index: 0 !important; /* ensure behind modal overlay */
}
.iconBtn:disabled {
opacity: 0.5;
cursor: default;
}
.iconBtn:hover:not(:disabled) {
background: var(--brand-sage-50, #F0F6F2);
border-color: var(--brand-sage, #6B8F71);
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
}
.title {
background-color: transparent;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-style: normal;
font-size: 20px;
color: rgba(88, 55, 50, 1);
text-align: left;
width: calc(70% - 10px);
padding-left: 10px;
text-transform: capitalize;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;
font-style: normal;
font-size: 22px;
color: rgba(88, 55, 50, 1);
text-align: left;
width: calc(70% - 12px);
padding-left: 12px;
text-transform: capitalize;
border: none;
outline: none;
position: relative;
z-index: 151;
}
.edit-typeItem-button {
margin-left: auto; /* Push the button to the right */
padding: 8px 16px; /* Adjust padding as needed */
margin-left: auto;
padding: 10px 16px;
font-size: 14px;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 4px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: relative;
z-index: 152;
}
.edit-typeItem-button:hover {
background-color: #0069d9;
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.add-item-button {
margin-top: 10px;
padding: 8px 16px; /* Adjust padding as needed */
font-size: 14px;
background-color: #359d42d1;
color: #fff;
margin: 0; /* follow parent gap for top/bottom spacing */
display: inline-block;
width: 275px; /* requested size */
height: 275px; /* requested size */
padding: 0; /* match image padding (none) */
border: none;
border-radius: 4px;
background: transparent; /* card drawn via ::before */
color: transparent; /* no visible text */
border-radius: 12px; /* match portrait image radius */
cursor: pointer;
transition: none;
box-shadow: none;
position: relative;
z-index: 1; /* behind modal overlay */
}
.add-item-button::before {
content: "";
position: absolute;
inset: 0; /* fill container */
border-radius: 12px; /* match portrait image radius */
background:
/* vertical bar */
linear-gradient(var(--brand-sage, #6B8F71), var(--brand-sage, #6B8F71)) center/5px 20% no-repeat,
/* horizontal bar */
linear-gradient(var(--brand-sage, #6B8F71), var(--brand-sage, #6B8F71)) center/20% 5px no-repeat,
/* base */
var(--brand-sage-50, #F0F6F2);
border: 2px dashed var(--brand-sage, #6B8F71);
box-shadow: 0 4px 12px rgba(0,0,0,0.10);
}
.add-item-button:focus-visible {
outline: 2px solid var(--brand-sage, #6B8F71);
outline-offset: 2px;
border-radius: 12px;
}
/* Overlay text inside the left tile area */
.add-item-button::after { content: none; }
.add-item-button:hover {
background-color: #ffffff;
box-shadow: none;
}
.item-list {
display: flex;
flex-direction: column; /* Display items in a column */
flex-direction: column;
gap: 16px;
position: relative;
z-index: 0 !important; /* ensure behind modal overlay */
}
/* Generic switch row styling reused in multiple spots */
.switch-container {
display: inline-flex;
align-items: center;
gap: 8px;
}
.switch-label {
font-size: 13px;
font-weight: 600;
color: #333;
}
/* Grid layout for portrait cards on cafe page */
.item-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
@media (min-width: 600px) {
.item-grid {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
}
@media (min-width: 992px) {
.item-grid {
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
}
.border {
@@ -132,40 +260,91 @@
.noborder {
border: 1px solid #ffffff00;
}
.itemWrapper {
position: relative;
background-color: #ffffff;
border-radius: 12px;
box-shadow: 0 1px 5px rgba(0,0,0,0.08);
transition: all 0.2s ease;
overflow: hidden;
position: relative;
z-index: 150;
}
.itemWrapper:hover { /* remove hover effect */ }
.editModeLayout {
border-radius: 4px;
/* Turn full-width bar into subtle corner controls over image */
position: absolute;
z-index: 100;
background-color: #0000008c;
width: 100%;
top: 7px;
bottom: -4px;
display: flex;
flex-direction: row;
inset: 0;
z-index: 155; /* above itemWrapper */
pointer-events: none; /* only children are interactive */
}
/* Left badge: availability switch + label */
.editModeLayout > div:first-child {
position: absolute;
top: 16px;
left: 16px;
display: inline-flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
gap: 8px;
background: rgba(0, 0, 0, 0.55);
color: #fff;
border-radius: 999px;
padding: 6px 10px;
pointer-events: auto;
backdrop-filter: blur(2px);
}
/* Right badge: edit/unmark button */
.editModeLayout > div:last-child {
position: absolute;
top: 16px;
right: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: rgba(0, 0, 0, 0.55);
color: #fff;
border-radius: 999px;
pointer-events: auto;
cursor: pointer;
transition: background 0.2s ease, transform 0.2s ease;
backdrop-filter: blur(2px);
}
.editModeLayout > div:last-child:hover {
background: rgba(0, 0, 0, 0.7);
transform: translateY(-1px);
}
/* When label exists, keep it compact and readable */
.editModeLayout .switch-label {
font-size: 12px;
font-weight: 600;
color: #fff;
white-space: nowrap;
}
.PaymentOption {
overflow-x: hidden;
background-color: #e9e9e9;
background-color: #ffffff;
display: flex;
flex-direction: column;
justify-content: center;
font-size: calc(10px + 2vmin);
font-size: calc(12px + 2vmin);
color: rgba(88, 55, 50, 1);
border-radius: 15px 15px 0 0;
border-radius: 20px 20px 0 0;
position: fixed;
bottom: 0;
right: 0;
left: 0;
z-index: 300;
z-index: 300; /* Menurunkan z-index agar tidak menutupi material list */
box-shadow: 0 -2px 20px rgba(0, 0, 0, 0.1);
padding: 20px;
}
.TotalContainer {
@@ -176,20 +355,28 @@
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;
font-style: normal;
font-size: 1.5em;
padding: 10px 0;
font-size: 1.8em;
padding: 15px 0;
border-bottom: 1px solid #e6e6e6;
}
.OptionContainer {
display: flex;
justify-content: space-between;
width: 80vw;
margin: 0 auto;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;
font-weight: 500;
font-style: normal;
font-size: 0.9em;
padding: 10px 0;
font-size: 1.1em;
padding: 15px 0;
align-items: center;
}
.OptionContainer span:first-child {
color: #555;
}
.PayButton {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
@@ -197,7 +384,7 @@
font-size: 32px;
width: 80vw;
height: 70px;
height: 56px;
border-radius: 50px;
background-color: rgba(88, 55, 50, 1);
color: white;
@@ -205,14 +392,32 @@
margin: 0px auto;
cursor: pointer;
margin-bottom: 23px;
transition: all 0.2s ease;
box-shadow: 0 4px 12px rgba(88, 55, 50, 0.2);
position: relative;
z-index: 301;
}
.PayButton:hover {
background-color: rgba(70, 45, 40, 1);
box-shadow: 0 6px 16px rgba(88, 55, 50, 0.3);
}
.Pay2Button {
text-align: center;
color: rgba(88, 55, 50, 1);
font-size: 1em;
margin-bottom: 25px;
font-size: 1.2em;
font-weight: 500;
margin-bottom: 10px;
cursor: pointer;
padding: 12px;
border-radius: 8px;
transition: all 0.2s ease;
position: relative;
z-index: 301;
}
.Pay2Button:hover {
background-color: #f0f0f0;
}
.item-list-title {
@@ -225,3 +430,24 @@
color: black;
text-align: left;
}
/* Harga dalam item list - dikurangi ukurannya agar proporsional */
.itemPriceList {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 700;
font-size: 14px;
color: #1c1d1d;
margin: 2px 0;
white-space: nowrap;
}
.itemPriceList.promo {
color: #e53935;
font-size: 13px;
}
.itemPriceList.original {
color: #727272;
font-size: 12px;
text-decoration: line-through;
}

View File

@@ -1,5 +1,6 @@
import React, { useRef, useEffect, useState } from "react";
import styles from "./ItemType.module.css";
import { Coffee, CupSoda, CakeSlice, Utensils, Grid2X2, Plus } from 'lucide-react';
export default function ItemType({
onClick,
@@ -9,6 +10,8 @@ export default function ItemType({
imageUrl,
selected,
rectangular,
compact,
noIcon, // New prop to remove icons
}) {
const inputRef = useRef(null);
const [namee, setName] = useState(name);
@@ -57,42 +60,64 @@ export default function ItemType({
onCreate(namee, selectedImage);
};
const formatName = (val) => {
if (!val || typeof val !== 'string') return val;
return val
.toLowerCase()
.replace(/\b\w/g, (c) => c.toUpperCase());
};
const iconImageUrl = imageUrl === 'uploads/assets/All.png' ? 'icon:all' : imageUrl;
// If noIcon is true, we render a button-like element without icons
if (noIcon) {
return (
<div
className={`${styles["item-type"]} ${compact ? styles["item-type-compact"] : ""}`}
style={{ zIndex: blank ? 301 : "inherit" }}
>
<div
onClick={onClick}
className={`${styles["item-type-rect"]} ${compact ? styles["item-type-rect-compact"] : ""} ${selected ? styles["selected"] : ""}`}
style={{
backgroundColor: selected ? 'var(--brand-sage, #6B8F71)' : '#ffffff',
borderColor: selected ? 'var(--brand-sage, #6B8F71)' : '#e6e6e6',
color: selected ? '#ffffff' : '#2d2d2d',
}}
>
<span>
{formatName(namee)}
</span>
</div>
</div>
);
}
return (
<div
className={
styles[
namee
? "item-type"
: rectangular
? "item-type-rectangular"
: "item-type-nomargin"
]
}
className={`${styles["item-type"]} ${compact ? styles["item-type-compact"] : ""}`}
style={{ zIndex: blank ? 301 : "inherit" }}
>
<div
onClick={
rectangular ? (blank ? null : () => onClick(imageUrl)) : onClick
rectangular ? (blank ? null : () => onClick(iconImageUrl)) : onClick
}
className={styles["item-type-rect"]}
className={`${styles["item-type-rect"]} ${compact ? styles["item-type-rect-compact"] : ""} ${selected ? styles["selected"] : ""}`}
style={{
top: selected ? "-10px" : "initial",
backgroundColor: selected ? 'var(--brand-sage, #6B8F71)' : '#ffffff',
borderColor: selected ? 'var(--brand-sage, #6B8F71)' : '#e6e6e6',
color: selected ? '#ffffff' : '#2d2d2d',
}}
>
{imageUrl != 'uploads/assets/All.png' ?
<img
src={previewUrl}
alt={namee}
className={styles["item-type-image"]}
/>
:<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
{iconImageUrl === 'uploads/assets/All.png' ? (
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="100%" height="100%" viewBox="0 0 800.000000 800.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.16, written by Peter Selinger 2001-2019
</metadata>
<g transform="translate(0.000000,800.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
fill="currentColor" stroke="none">
<path d="M3708 7165 c-3 -4 -44 -10 -90 -15 -266 -28 -530 -91 -753 -180 -11
-4 -42 -16 -70 -26 -27 -9 -129 -57 -225 -106 -186 -94 -188 -95 -262 -145
-26 -18 -52 -33 -58 -33 -5 0 -24 -13 -42 -28 -18 -16 -53 -43 -78 -61 -124
@@ -138,7 +163,17 @@ c261 0 329 -3 352 -14z m1237 -2 c52 -35 54 -49 54 -379 0 -348 -2 -360 -69
58 40 59 387 60 178 0 328 -4 342 -10z"/>
</g>
</svg>
}
) : (iconImageUrl && typeof iconImageUrl === 'string' && iconImageUrl.startsWith('icon:')) ? (
<div style={{width:'100%',height:'100%',display:'flex',alignItems:'center',justifyContent:'center'}}>
<LucideCategoryIcon name={namee} iconKey={(iconImageUrl || '').split(':')[1]} compact={compact} />
</div>
) : (
<img
src={previewUrl}
alt={namee}
className={styles["item-type-image"]}
/>
)}
{blank && rectangular && (
<div className={styles["item-type-image-container"]}>
<input
@@ -152,18 +187,42 @@ c261 0 329 -3 352 -14z m1237 -2 c52 -35 54 -49 54 -379 0 -348 -2 -360 -69
)}
</div>
{!rectangular && !blank && (
<input
ref={inputRef}
className={`${styles["item-type-name"]} ${styles.noborder}`}
value={namee}
onChange={handleNameChange}
disabled={true}
style={{
top: selected ? "-5px" : "initial",
borderBottom: selected ? "1px solid #000" : "none",
}}
/>
<div className={`${styles["item-type-name"]} ${compact ? styles["item-type-name-compact"] : ""}`} style={{ color: selected ? '#2d2d2d' : '#333' }}>
{formatName(namee)}
</div>
)}
</div>
);
}
function LucideCategoryIcon({ name, iconKey, compact }) {
const key = pickIconKey(name, iconKey);
const size = compact ? '65%' : '56%';
switch (key) {
case 'coffee':
return <Coffee color={'currentColor'} size={size} strokeWidth={2} />;
case 'drink':
return <CupSoda color={'currentColor'} size={size} strokeWidth={2} />;
case 'dessert':
return <CakeSlice color={'currentColor'} size={size} strokeWidth={2} />;
case 'food':
return <Utensils color={'currentColor'} size={size} strokeWidth={2} />;
case 'all':
return <Grid2X2 color={'currentColor'} size={size} strokeWidth={2} />;
case 'plus':
return <Plus color={'currentColor'} size={size} strokeWidth={2} />;
default:
return <Utensils color={'currentColor'} size={size} strokeWidth={2} />;
}
}
function pickIconKey(name, iconKey) {
const n = (name || '').toLowerCase();
if (iconKey === 'plus') return 'plus';
if (iconKey === 'all') return 'all';
if (/(kopi|coffee|espresso|latte|americano|kapal|brew)/.test(n)) return 'coffee';
if (/(teh|tea|drink|minum|soda|juice|jus|milk|susu|lemon)/.test(n)) return 'drink';
if (/(dessert|cake|kue|manis|ice|es krim|ice-cream)/.test(n)) return 'dessert';
if (/(food|makan|snack|cemilan|nasi|mie|noodle|soup|sup|ayam|daging|ikan|roti|sandwich|burger|pizza)/.test(n)) return 'food';
return 'food';
}

View File

@@ -1,7 +1,7 @@
.item-type {
width: calc(25vw - 20px);
height: calc(30vw - 20px);
margin: 1px 10px 0px;
width: auto;
height: auto;
margin: 0 6px;
overflow: visible;
text-align: center;
align-items: center;
@@ -10,55 +10,107 @@
justify-content: center;
position: relative;
}
.item-type-rectangular {
width: calc(25vw - 20px);
height: calc(25vw - 20px);
overflow: visible;
text-align: center;
align-items: center;
display: flex;
flex-direction: column;
justify-content: start;
position: relative; /* Ensure absolute positioning inside works */
/* Compact version of item-type */
.item-type-compact {
margin: 0 4px;
}
.item-type-nomargin {
width: calc(25vw - 20px);
height: calc(39vw - 20px);
.item-type-rectangular {
width: calc(30vw - 24px);
height: calc(30vw - 24px);
overflow: visible;
text-align: center;
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
position: relative; /* Ensure absolute positioning inside works */
position: relative;
}
.item-type-nomargin {
width: calc(30vw - 24px);
height: calc(45vw - 24px);
overflow: visible;
text-align: center;
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
}
.item-type-rect {
position: relative;
height: 13vw;
width: 13vw;
height: 48px; /* Fixed height for better touch targets */
min-width: 100px; /* Minimum width */
padding: 0 20px; /* Horizontal padding */
object-fit: cover;
border-radius: 15px;
border-radius: 12px; /* Square rounded corners */
background-color: #fff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
border: 1px solid #e6e6e6;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-size: 16px;
color: #2d2d2d;
cursor: pointer;
}
/* Compact version of item-type-rect */
.item-type-rect-compact {
height: 42px; /* Slightly smaller for compact version */
min-width: 90px;
padding: 0 18px;
border-radius: 10px;
font-size: 14px;
}
.item-type-rect:hover {
background-color: var(--brand-sage-100, #E9F3ED);
border-color: var(--brand-sage, #6B8F71);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
}
.item-type-rect.selected {
background-color: var(--brand-sage, #6B8F71);
color: #ffffff;
border-color: var(--brand-sage, #6B8F71);
box-shadow: 0 2px 6px rgba(107, 143, 113, 0.2);
}
.item-type-name {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-style: normal;
font-size: 14px;
color: #333;
width: calc(25vw - 30px);
font-size: 16px;
color: #2d2d2d;
width: auto;
text-align: center;
background-color: transparent;
position: relative; /* Needed for positioning the button */
position: relative;
margin-top: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 160px;
}
/* Compact version of item-type-name */
.item-type-name-compact {
font-size: 14px;
max-width: 140px;
}
.item-type-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 15px;
border-radius: 12px;
}
.item-type-image-container {
@@ -68,6 +120,7 @@
width: 100%;
height: 100%;
}
.item-type-image-input {
opacity: 0;
position: absolute;
@@ -79,12 +132,12 @@
.item-type-create {
position: absolute;
top: 76%; /* Position below the input */
top: 76%;
left: 50%;
transform: translateX(-50%);
margin-top: 10px; /* Space between input and button */
width: 20vw;
text-align: center; /* Center button text */
margin-top: 12px;
width: 24vw;
text-align: center;
}
.border {
@@ -94,3 +147,17 @@
.noborder {
border: 1px solid #ffffff00;
}
/* No icon styles */
.no-icon {
height: 48px;
min-width: 100px;
padding: 0 20px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 500;
font-family: "Plus Jakarta Sans", sans-serif;
}

View File

@@ -1,23 +1,95 @@
/* ItemTypeLister.css */
/* New clean, intuitive category bar */
.item-type-lister {
width: 100vw;
width: 100%;
overflow-x: auto;
white-space: nowrap;
padding: 3px 0px;
margin-bottom: -5px;
padding: 8px 0; /* Reduced padding for more compact design */
margin-bottom: 8px; /* Reduced margin for more compact design */
scrollbar-width: thin;
display: flex;
justify-content: center;
align-items: center;
}
.item-type-lister::-webkit-scrollbar {
height: 6px;
}
.item-type-lister::-webkit-scrollbar-thumb {
background-color: #c5c5c5;
border-radius: 3px;
}
.category-bar {
display: flex;
align-items: center;
gap: 6px; /* Reduced gap for more compact design */
overflow-x: auto;
padding: 6px 10px; /* Reduced padding */
-ms-overflow-style: none;
scrollbar-width: none;
justify-content: center;
width: 100%;
}
.category-bar::-webkit-scrollbar { display: none; }
/* Legacy horizontal tile list container (used for tile UI) */
.item-type-list {
display: inline-flex;
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none;
scrollbar-width: none;
overflow-y: hidden;
gap: 6px; /* Added gap for consistent spacing */
justify-content: center;
align-items: center;
width: 100%;
}
.item-type {
display: inline-block;
margin-right: 20px;
/* Space between items */
.category-chip {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 6px;
height: 32px; /* Reduced height for more compact design */
padding: 0 14px; /* Reduced padding for more compact design */
border-radius: 999px;
border: 1px solid #e6e6e6;
background: #ffffff;
color: #2d2d2d;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-size: 13px; /* Slightly smaller font */
cursor: pointer;
user-select: none;
transition: all 0.2s ease;
justify-content: center;
}
.category-chip:hover {
border-color: #d0d0d0;
background-color: #f8f8f8;
}
.category-chip.selected {
background: #73a585;
color: #ffffff;
border-color: #73a585;
}
.category-chip .chip-icon {
width: 16px; /* Reduced icon size */
height: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.add-item-chip {
background: #f4f7f5;
border-color: #dfe7e2;
color: #4a6b5a;
}
.add-item-chip:hover { background: #eaf1ed; }
.rect-creator {
width: 100vw;
height: 100vh;
@@ -25,7 +97,7 @@
top: 0;
right: 0;
background-color: white;
z-index: 300;
z-index: 0; /* align with item lister */
display: flex;
flex-direction: column;
}
@@ -55,13 +127,123 @@
bottom: 0;
align-self: center; /* Center the button horizontally */
}
.item-type-name {
font-family: "Plus Jakarta Sans", sans-serif;
font-style: normal;
height: 20vw;
font-size: 1.5rem;
font-weight: 500;
color: black;
text-transform: capitalize;
z-index: 301;
/* Legacy styles kept for ItemType grid if needed elsewhere */
/* Compact centered item type list without icon tiles */
.compact-centered-list {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
padding: 8px 0;
overflow-x: auto;
}
.compact-item-type {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
height: 36px;
padding: 0 16px;
border-radius: 999px;
border: 1px solid #e6e6e6;
background: #ffffff;
color: #2d2d2d;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-size: 14px;
cursor: pointer;
user-select: none;
transition: all 0.2s ease;
margin: 0 4px;
white-space: nowrap;
}
.compact-item-type:hover {
border-color: #d0d0d0;
background-color: #f8f8f8;
}
.compact-item-type.selected {
background: #73a585;
color: #ffffff;
border-color: #73a585;
}
.compact-add-item {
background: #f4f7f5;
border-color: #dfe7e2;
color: #4a6b5a;
}
.compact-add-item:hover {
background: #eaf1ed;
}
/* Responsive design */
@media (max-width: 768px) {
.item-type-lister {
padding: 6px 0;
margin-bottom: 6px;
}
.category-bar {
gap: 4px;
padding: 4px 8px;
}
.item-type-list {
gap: 4px;
}
.category-chip {
height: 28px;
padding: 0 12px;
font-size: 12px;
}
.category-chip .chip-icon {
width: 14px;
height: 14px;
}
.compact-item-type {
height: 32px;
padding: 0 14px;
font-size: 13px;
margin: 0 3px;
}
}
@media (max-width: 480px) {
.item-type-lister {
padding: 4px 0;
margin-bottom: 4px;
}
.category-bar {
gap: 3px;
padding: 3px 6px;
}
.item-type-list {
gap: 3px;
}
.category-chip {
height: 26px;
padding: 0 10px;
font-size: 11px;
}
.category-chip .chip-icon {
width: 12px;
height: 12px;
}
.compact-item-type {
height: 30px;
padding: 0 12px;
font-size: 12px;
margin: 0 2px;
}
}

View File

@@ -1,8 +1,7 @@
import React, { useState, useRef, useEffect } from "react";
import smoothScroll from "smooth-scroll-into-view-if-needed";
import "./ItemTypeLister.css";
import ItemType from "./ItemType";
import { createItem, createItemType } from "../helpers/itemHelper.js";
import { createItem } from "../helpers/itemHelper.js";
import { getImageUrl } from "../helpers/itemHelper";
import ItemLister from "./ItemLister";
@@ -22,31 +21,13 @@ const ItemTypeLister = ({
const newItemDivRef = useRef(null);
const [items, setItems] = useState([]);
const [itemTypeName, setItemTypeName] = useState("");
const handleCreateItem = (name, price, selectedImage, previewUrl, description, promoPrice) => {
console.log(previewUrl);
const newItem = {
itemId: items.length + 1,
name,
price,
selectedImage,
image: previewUrl,
availability: true,
description,
promoPrice
};
// Update the items state with the new item
setItems((prevItems) => [...prevItems, newItem]);
};
// Effect to handle changes to isAddingNewItem
useEffect(() => {
if (isAddingNewItem && newItemDivRef.current) {
// Use smooth-scroll-into-view-if-needed to scroll to the target div
smoothScroll(newItemDivRef.current, {
behavior: "smooth",
block: "start", // Adjust this based on your needs
block: "start",
inline: "nearest",
});
} else {
@@ -54,11 +35,11 @@ const ItemTypeLister = ({
smoothScroll(node, {
behavior: "smooth",
block: "start", // Adjust this based on your needs
block: "start",
inline: "nearest",
});
}
}, [isAddingNewItem]); // Dependency array includes isAddingNewItem
}, [isAddingNewItem]);
const toggleAddNewItem = () => {
setIsAddingNewItem((prev) => !prev);
@@ -67,90 +48,66 @@ const ItemTypeLister = ({
document.body.style.overflow = !isAddingNewItem ? "hidden" : "auto";
};
async function handleCreate(name, selectedImage) {
createItemType(shopId, name, selectedImage);
}
// Removed legacy image upload logic used by the old tile view
const [selectedImage, setSelectedImage] = useState(null);
const [previewUrl, setPreviewUrl] = useState("");
const [imageUrl, setImaguUrl] = useState("");
const canManage = user && (user.user_id == shopOwnerId || user.cafeId == shopId);
useEffect(() => {
// if (selectedImage) {
// const reader = new FileReader();
// reader.onloadend = () => {
// setPreviewUrl(reader.result);
// };
// reader.readAsDataURL(selectedImage);
// } else {
// setPreviewUrl(getImageUrl(imageUrl));
setPreviewUrl(selectedImage);
// }
}, [selectedImage, imageUrl]);
const handleImageChange = (e) => {
setSelectedImage(e);
const formatName = (name) => {
if (!name) return name;
return name
.toLowerCase()
.replace(/\b\w/g, (c) => c.toUpperCase());
};
return (
<div
className="item-type-lister"
style={{ overflowX: isAddingNewItem ? "hidden" : "" }}
>
<div
ref={newItemDivRef}
className="item-type-list"
style={{ display: isAddingNewItem ? "inline-flex" : "inline-flex" }}
>
{isEditMode &&
!isAddingNewItem &&
user && (
user.userId == shopOwnerId || user.cafeId == shopId) && (
<ItemType
<div className="item-type-lister" style={{ overflowX: isAddingNewItem ? 'hidden' : 'auto' }}>
<div className="compact-centered-list">
<div ref={newItemDivRef} className="compact-item-type-list" style={{ display: 'inline-flex' }}>
{isEditMode && !isAddingNewItem && canManage && (
<div
className="compact-item-type compact-add-item"
onClick={toggleAddNewItem}
name={"buat baru"}
imageUrl={getImageUrl("uploads/assets/addnew.png")}
>
Buat baru
</div>
)}
{canManage && isAddingNewItem && (
<ItemLister
shopId={shopId}
shopOwnerId={shopOwnerId}
user={user}
typeName={""}
setShopItems={setShopItems}
itemList={items}
isEditMode={true}
handleCreateItem={(itemTypeId, name, price, selectedImage, description, promoPrice) => createItem(shopId, name, price, selectedImage, itemTypeId, description, promoPrice)}
beingEditedType={beingEditedType}
setBeingEditedType={setBeingEditedType}
alwaysEdit={true}
handleUnEdit={toggleAddNewItem}
/>
)}
{user &&(
user.userId == shopOwnerId || user.cafeId == shopId) &&
isAddingNewItem && (
<>
<ItemLister
shopId={shopId}
shopOwnerId={shopOwnerId}
user={user}
typeName={""}
setShopItems={setShopItems}
itemList={items}
isEditMode={true}
handleCreateItem={(itemTypeId, name, price, selectedImage, description, promoPrice) => createItem(shopId, name, price, selectedImage, itemTypeId, description, promoPrice)}
beingEditedType={beingEditedType}
setBeingEditedType={setBeingEditedType}
alwaysEdit={true}
handleUnEdit={toggleAddNewItem}
/>
</>
)}
{itemTypes && itemTypes.length > 0 && (
<ItemType
name={"semua"}
onClick={() => onFilterChange(0)}
imageUrl={"uploads/assets/All.png"}
/>
)}
{itemTypes &&
itemTypes.map(
(itemType) =>
(
itemType.itemList.length > 0 || (user && (user.userId == shopOwnerId || user.cafeId == shopId))) && (
<ItemType
key={itemType.itemTypeId}
name={itemType.name}
imageUrl={getImageUrl(itemType.image)}
onClick={() => onFilterChange(itemType.itemTypeId)}
selected={filterId === itemType.itemTypeId}
/>
)
{itemTypes && itemTypes.length > 0 && (
<div
className={`compact-item-type ${filterId === 0 ? 'selected' : ''}`}
onClick={() => onFilterChange(0)}
>
Semua
</div>
)}
{itemTypes && itemTypes.map((itemType) => (
<div
key={itemType.itemTypeId}
className={`compact-item-type ${filterId === itemType.itemTypeId ? 'selected' : ''}`}
onClick={() => onFilterChange(itemType.itemTypeId)}
>
{formatName(itemType.name)}
</div>
))}
</div>
</div>
</div>
);

View File

@@ -1,11 +1,12 @@
import React, {useState, useEffect} from "react";
import { createPortal } from "react-dom";
import styles from "./Modal.module.css";
import AccountUpdatePage from "../components/AccountUpdatePage.js";
import CreateClerk from "../pages/CreateClerk"
import CreateCafe from "../pages/CreateCafe"
import CreateTenant from "../pages/CreateTenant"
import TablesPage from "./TablesPage.js";
import IdentifyCafeModal from "./IdentifyCafeModal.js";
import PaymentOptions from "./PaymentOptions.js";
import Transaction from "../pages/Transaction";
import Transaction_item from "../pages/Transaction_item";
@@ -75,9 +76,9 @@ const Modal = ({ user, shop, isOpen, onClose, modalContent, deviceType, setModal
event.stopPropagation();
};
if(modalContent == '') handleOverlayClick();
return (
return createPortal(
<div key={updateKey} onClick={handleOverlayClick} className={styles.modalOverlay}>
<div className={styles.modalContent} onClick={handleContentClick}>
<div className={`${styles.modalContent} ${(modalContent === 'edit_tables' || modalContent === 'payment_option' || modalContent === 'create_clerk') ? styles.modalContentWide : ''}`} onClick={handleContentClick}>
{modalContent === "edit_account" && <AccountUpdatePage user={user} />}
{modalContent === "reset-password" && <ResetPassword />}
@@ -86,9 +87,9 @@ const Modal = ({ user, shop, isOpen, onClose, modalContent, deviceType, setModal
{modalContent === "create_clerk" && <CreateClerk shopId={shop.cafeId} />}
{modalContent === "create_kedai" && <CreateCafe shopId={shop.cafeId} />}
{modalContent === "create_tenant" && <CreateTenant shopId={shop.cafeId} />}
{modalContent === "edit_tables" && <TablesPage shop={shop} />}
{modalContent === "edit_tables" && <IdentifyCafeModal shop={shop} />}
{modalContent === "new_transaction" && (
<Transaction propsShopId={shop.cafeId} handleMoveToTransaction={handleMoveToTransaction} depth={depth} shopImg={shopImg} />
<Transaction propsShopId={shop.cafeId} handleMoveToTransaction={handleMoveToTransaction} depth={depth} shopImg={shopImg} setModal={setModal}/>
)}
{modalContent === "transaction_canceled" && (
<Transaction propsShopId={shop.cafeId} />
@@ -132,7 +133,8 @@ const Modal = ({ user, shop, isOpen, onClose, modalContent, deviceType, setModal
{modalContent === "message" && <Message handleYes={onModalYesFunction} handleNo={handleNo}/>}
{modalContent === "player-prompt" && <PlayerPrompt cafeId={shop.cafeId} setModal={setModal} handleClose={handleOverlayClick} welcomePageConfig={shop.welcomePageConfig}/>}
</div>
</div>
</div>,
document.body
);
};

View File

@@ -2,37 +2,295 @@
position: fixed;
top: 0;
left: 0;
right: -1px;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
z-index: 2147483647 !important; /* ensure above any app layers */
padding: 20px;
}
.modalContent {
width: 80vw;
max-height: 80vh;
width: 100%;
max-width: 500px;
max-height: 90vh;
background: #ffffff;
border-radius: 16px;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
animation: modalAppear 0.3s ease-out;
position: relative;
overflow: visible; /* Add this line to enable scrolling */
z-index: 10000; /* ensure above any page overlays */
}
.modalContentWide {
max-width: 920px;
}
@keyframes modalAppear {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.imageSection {
padding: 24px 24px 16px 24px;
background: #f8f9fa;
border-bottom: 1px solid #e6e6e6;
}
.imagePreview {
width: 100%;
height: 200px;
border-radius: 12px;
overflow: hidden;
margin-bottom: 20px;
background: #e9ecef;
display: flex;
align-items: center;
justify-content: center;
}
.previewImage {
width: 100%;
height: 100%;
object-fit: cover;
}
.imageActions {
display: flex;
gap: 12px;
}
.actionButton {
flex: 1;
padding: 12px 16px;
border-radius: 10px;
border: none;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.actionButton:not(.deleteButton) {
background-color: var(--brand-sage, #6B8F71);
color: white;
box-shadow: 0 2px 6px rgba(107, 143, 113, 0.2);
}
.actionButton:not(.deleteButton):hover {
background-color: #5a7a60;
box-shadow: 0 4px 10px rgba(107, 143, 113, 0.3);
}
.deleteButton {
background-color: #ff4d4d;
color: white;
box-shadow: 0 2px 6px rgba(255, 77, 77, 0.2);
}
.deleteButton:hover {
background-color: #ff1a1a;
box-shadow: 0 4px 10px rgba(255, 77, 77, 0.3);
}
.formSection {
padding: 24px;
overflow-y: auto;
flex: 1;
}
.formGroup {
margin-bottom: 20px;
}
.formRow {
display: flex;
gap: 16px;
}
.formRow .formGroup {
flex: 1;
margin-bottom: 0;
}
.formLabel {
display: block;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;
font-size: 14px;
color: #333;
margin-bottom: 8px;
}
.formInput {
width: 100%;
padding: 12px 16px;
border-radius: 10px;
border: 1px solid #ddd;
background: #ffffff;
font-family: "Plus Jakarta Sans", sans-serif;
font-size: 15px;
color: #333;
transition: all 0.2s ease;
box-sizing: border-box;
}
.formInput:focus {
outline: none;
border-color: var(--brand-sage, #6B8F71);
box-shadow: 0 0 0 3px rgba(107, 143, 113, 0.1);
}
.formTextarea {
width: 100%;
padding: 12px 16px;
border-radius: 10px;
border: 1px solid #ddd;
background: #ffffff;
font-family: "Plus Jakarta Sans", sans-serif;
font-size: 15px;
color: #333;
transition: all 0.2s ease;
box-sizing: border-box;
min-height: 100px;
resize: vertical;
}
.formTextarea:focus {
outline: none;
border-color: var(--brand-sage, #6B8F71);
box-shadow: 0 0 0 3px rgba(107, 143, 113, 0.1);
}
.formActions {
display: flex;
gap: 12px;
margin-top: 10px;
}
.bannerRow {
display: flex;
justify-content: flex-end;
margin-bottom: 10px;
}
.banner {
padding: 8px 12px;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
}
.bannerSuccess {
color: #155724;
background: #d4edda;
border: 1px solid #c3e6cb;
}
.bannerError {
color: #721c24;
background: #f8d7da;
border: 1px solid #f5c6cb;
}
.formButton {
flex: 1;
padding: 14px 16px;
border-radius: 10px;
border: none;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;
font-size: 15px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.cancelButton {
background-color: #f0f0f0;
color: #333;
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
}
.cancelButton:hover {
background-color: #e0e0e0;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.saveButton {
background-color: var(--brand-sage, #6B8F71);
color: white;
box-shadow: 0 2px 6px rgba(107, 143, 113, 0.2);
}
.saveButton:hover {
background-color: #5a7a60;
box-shadow: 0 4px 10px rgba(107, 143, 113, 0.3);
}
.closeButton {
position: absolute;
top: 10px;
right: 10px;
top: 16px;
right: 16px;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #333;
padding: 0;
color: #999;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s ease;
z-index: 1001; /* Menambah z-index agar selalu di atas modal */
}
.closeButton:hover {
color: #f44336; /* Change color on hover for better UX */
background: #f0f0f0;
color: #333;
}
/* Responsive design */
@media (max-width: 600px) {
.modalContent {
margin: 10px;
max-height: 95vh;
}
.imageSection {
padding: 20px;
}
.formSection {
padding: 20px;
}
.formRow {
flex-direction: column;
gap: 0;
}
.formActions {
flex-direction: column;
}
}

View File

@@ -31,14 +31,14 @@
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
border-radius: 0 0 15px 15px;
z-index: 1;
z-index: 0; /* align with item lister */
}
.current-name {
white-space: nowrap;
pointer-events: none;
position: relative;
z-index: 2;
z-index: 1;
text-align: left;
margin: 35px 30px;
font-size: 16px;
@@ -70,7 +70,7 @@
.current-artist {
pointer-events: none;
position: relative;
z-index: 2;
z-index: 1;
text-align: left;
margin: -32px 30px;
font-size: 18px;
@@ -83,7 +83,7 @@
.progress-container {
pointer-events: none;
position: relative;
z-index: 2;
z-index: 1;
text-align: left;
margin: 12px 30px;
}
@@ -318,4 +318,3 @@
.search-button.clicked {
background-color: #d0c7b3; /* The color when clicked */
}

View File

@@ -479,7 +479,7 @@ export function MusicPlayer({ socket, shopId, user, shopOwnerId, isSpotifyNeedLo
className={`expandable-container ${expanded ? "expanded" : ""}`}
ref={expandableContainerRef}
>
{user.cafeId == shopId || user.userId == shopOwnerId && (
{user.cafeId == shopId || user.user_id == shopOwnerId && (
<>
<div className="auth-box">
<div

View File

@@ -1,18 +1,13 @@
import React, { useState, useRef, useEffect } from "react";
import jsQR from "jsqr";
import QrScanner from "qr-scanner"; // Import qr-scanner
import { getImageUrl } from "../helpers/itemHelper";
import {
getCafe,
saveCafeDetails,
setConfirmationStatus,
setOpenBillAvailability
} from "../helpers/cafeHelpers";
import { getCafe, saveCafeDetails } from "../helpers/cafeHelpers";
import Switch from "react-switch"; // Import the Switch component
import styles from "./PaymentOptions.module.css";
const SetPaymentQr = ({ shopId,
qrCodeUrl }) => {
const [qrPosition, setQrPosition] = useState([50, 50]);
const [qrSize, setQrSize] = useState(50);
const SetPaymentQr = ({ shopId, qrCodeUrl }) => {
const [qrPosition, setQrPosition] = useState([50, 50]); // legacy kept for API compatibility
const [qrSize, setQrSize] = useState(50); // legacy kept for API compatibility
const [qrPayment, setQrPayment] = useState();
const [qrPaymentFile, setQrPaymentFile] = useState();
const [qrCodeDetected, setQrCodeDetected] = useState(false);
@@ -20,10 +15,15 @@ const SetPaymentQr = ({ shopId,
const [isQRISavailable, setIsQRISavailable] = useState(0);
const qrPaymentInputRef = useRef(null);
const qrCodeContainerRef = useRef(null);
const [qrCodeData, setQrCodeData] = useState(null);
const [cafe, setCafe] = useState({});
const [isConfigQRIS, setIsConfigQRIS] = useState(false);
const [isOpenBillAvailable, setIsOpenBillAvailable] = useState(false);
const [saving, setSaving] = useState(false);
const [saveStatus, setSaveStatus] = useState(null); // 'success' | 'error'
const [copied, setCopied] = useState(false);
useEffect(() => {
const fetchCafe = async () => {
@@ -45,12 +45,6 @@ const SetPaymentQr = ({ shopId,
fetchCafe();
}, [shopId]);
// Detect QR code when qrPayment updates
useEffect(() => {
if (qrPayment && isConfigQRIS) {
detectQRCodeFromContainer();
}
}, [qrPayment, isConfigQRIS]);
// Handle file input change
const handleFileChange = (e) => {
@@ -62,29 +56,54 @@ const SetPaymentQr = ({ shopId,
}
};
// Detect QR code from the container
useEffect(() => {
if (qrPayment && isConfigQRIS) {
detectQRCodeFromContainer();
}
}, [qrPayment, isConfigQRIS]);
const detectQRCodeFromContainer = () => {
const container = qrCodeContainerRef.current;
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
if (!container) return;
const img = new Image();
img.crossOrigin = "Anonymous";
img.onload = () => {
canvas.width = container.offsetWidth;
canvas.height = container.offsetHeight;
context.drawImage(img, 0, 0, canvas.width, canvas.height);
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const qrCode = jsQR(imageData.data, canvas.width, canvas.height);
setQrCodeDetected(!!qrCode);
if (qrCode) {
console.log("QR Code detected:", qrCode.data);
}
};
img.src = qrPayment;
img.onload = () => {
// Buat canvas dari image
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
canvas.width = img.width;
canvas.height = img.height;
context.drawImage(img, 0, 0);
// Ambil data URL dari canvas (png)
const imageDataUrl = canvas.toDataURL();
QrScanner.scanImage(imageDataUrl, { returnDetailedScanResult: true })
.then(result => {
setQrCodeDetected(true);
setQrCodeData(result.data);
console.log("QR Code detected:", result.data);
})
.catch(() => {
setQrCodeDetected(false);
setQrCodeData(null);
console.log("QR Code not detected");
});
};
img.onerror = () => {
setQrCodeDetected(false);
setQrCodeData(null);
};
};
// Save cafe details
const handleSave = async () => {
setSaving(true);
setSaveStatus(null);
let qrPaymentFileCache;
console.log(qrPaymentFile)
if(qrPaymentFile != null)
@@ -101,240 +120,141 @@ const SetPaymentQr = ({ shopId,
try {
const response = await saveCafeDetails(cafe.cafeId, details);
setIsNeedConfirmationState(response.needsConfirmation ? 1 : 0); // Update state after saving
setIsQRISavailable(response.isQRISavailable ? 1 : 0); // Update state after saving
setIsOpenBillAvailable(response.isOpenBillAvailable ? 1 : 0); // Update state after saving
setIsNeedConfirmationState(response.needsConfirmation ? 1 : 0);
setIsQRISavailable(response.isQRISavailable ? 1 : 0);
setIsOpenBillAvailable(response.isOpenBillAvailable ? 1 : 0);
setSaveStatus('success');
console.log("Cafe details saved:", response);
} catch (error) {
console.error("Error saving cafe details:", error);
setSaveStatus('error');
} finally {
setSaving(false);
}
};
const copyQrData = async () => {
if (!qrCodeData) return;
try {
await navigator.clipboard.writeText(qrCodeData);
setCopied(true);
setTimeout(()=>setCopied(false), 1200);
} catch {}
};
return (
<div style={styles.container}>
<h3 style={styles.title}>Konfigurasi pembayaran</h3>
<div className={styles.container}>
<h3 className={styles.title}>Konfigurasi pembayaran</h3>
<div style={styles.switchContainer}>
<p style={styles.uploadMessage}>
Pembayaran QRIS.
</p>
<div className={styles.section}>
<div className={styles.sectionHeader}>
<div>
<div className={styles.sectionTitle}>Pembayaran QRIS</div>
<div className={styles.sectionDesc}>Aktifkan agar pelanggan dapat membayar via QRIS. Kasir tetap perlu verifikasi rekening.</div>
</div>
<Switch
onChange={(checked) => setIsQRISavailable(checked ? 1 : 0)}
checked={isQRISavailable === 1}
offColor="#888"
onColor="#4CAF50"
uncheckedIcon={false}
checkedIcon={false}
height={25}
width={50}
/>
</div>
{isConfigQRIS ?
<div className={styles.row}>
<button
type="button"
className={`${styles.button} ${styles.configButton}`}
onClick={() => isQRISavailable === 1 && setIsConfigQRIS(true)}
disabled={isQRISavailable !== 1}
>
Konfigurasi QRIS
</button>
</div>
{isConfigQRIS && (
<>
<div
id="qr-code-container"
ref={qrCodeContainerRef}
className={styles.imageBox}
onClick={() => qrPaymentInputRef.current.click()}
style={{
...styles.qrCodeContainer,
backgroundImage: `url(${qrPayment})`,
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
backgroundSize: "contain",
}}
style={{ backgroundImage: `url(${qrPayment})` }}
>
<input
type="file"
accept="image/*"
ref={qrPaymentInputRef}
style={{ display: "none" }}
onChange={handleFileChange}
/>
<input type="file" accept="image/*" ref={qrPaymentInputRef} style={{ display: 'none' }} onChange={handleFileChange} />
</div>
<div style={styles.uploadMessage}>
<p>Klik untuk ganti background</p>
</div>
<div style={styles.resultMessage}>
{qrCodeDetected && qrPayment !== 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPsNr0TPq8dHT3nBwDQ6OHQQTrqzVFoeBOmuWfgyErrLbJi6f6CnnYhpNHEvkJ_2X-kyI&usqp=CAU' ? <p>QR terdeteksi</p> : <p>Tidak ada qr terdeteksi</p>}
{qrCodeDetected && qrPayment !== 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPsNr0TPq8dHT3nBwDQ6OHQQTrqzVFoeBOmuWfgyErrLbJi6f6CnnYhpNHEvkJ_2X-kyI&usqp=CAU' ? <div
onClick={() => qrPaymentInputRef.current.click()} style={styles.uploadButton}>Ganti</div> : <div
onClick={() => qrPaymentInputRef.current.click()} style={styles.uploadButton}>Unggah</div>}
</div>
<div onClick={() => setIsConfigQRIS(false)}
style={{
...styles.qrisConfigButton,
width: '100%',
marginLeft: "0",
}}
>Terapkan</div>
</>
:
<>
<p style={styles.description}>
Aktifkan fitur agar pelanggan dapat menggunakan opsi pembayaran QRIS, namun kasir anda perlu memeriksa rekening untuk memastikan pembayaran.
</p>
<div style={{ display: 'flex' }}>
<Switch
onChange={(checked) => setIsQRISavailable(checked ? 1 : 0)}
checked={isQRISavailable === 1} // Convert to boolean
offColor="#888"
onColor="#4CAF50"
uncheckedIcon={false}
checkedIcon={false}
height={25}
width={50}
/>
<div
onClick={() => setIsConfigQRIS(true)}
style={{
...styles.qrisConfigButton,
backgroundColor: isQRISavailable == 1 ? styles.qrisConfigButton.backgroundColor : 'gray',
}}
>
Konfigurasi QRIS
<div className={styles.smallNote}>Klik area untuk unggah/ganti gambar QR</div>
<div className={styles.detectRow}>
<div className={`${styles.tag} ${qrCodeDetected ? styles.tagOk : styles.tagBad}`}>
{qrCodeDetected && qrPayment !== 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPsNr0TPq8dHT3nBwDQ6OHQQTrqzVFoeBOmuWfgyErrLbJi6f6CnnYhpNHEvkJ_2X-kyI&usqp=CAU' ? 'QR terdeteksi' : 'Tidak ada QR terdeteksi'}
</div>
<button className={styles.button} onClick={() => qrPaymentInputRef.current.click()}>{qrPayment ? 'Ganti' : 'Unggah'}</button>
</div>
{qrCodeDetected && (
<div className={styles.copyRow}>
<input className={styles.linkField} readOnly value={qrCodeData || ''} />
<button className={styles.button} onClick={copyQrData}>{copied ? 'Disalin' : 'Salin'}</button>
</div>
)}
<div className={styles.actionsRight}>
<button className={`${styles.button} ${styles.primary}`} onClick={() => setIsConfigQRIS(false)}>Terapkan</button>
</div>
</>
}
</div>
<div style={styles.switchContainer}>
<p style={styles.uploadMessage}>
Open bill
</p>
<p style={styles.description}>
Aktifkan fitur agar pelanggan dapat menambahkan pesanan selama sesi berlangsung tanpa perlu melakukan transaksi baru dan hanya membayar di akhir.
</p>
<Switch
onChange={(checked) => setIsOpenBillAvailable(checked ? 1 : 0)}
checked={isOpenBillAvailable === 1} // Convert to boolean
offColor="#888"
onColor="#4CAF50"
uncheckedIcon={false}
checkedIcon={false}
height={25}
width={50}
/>
)}
</div>
<div style={styles.switchContainer}>
<p style={styles.uploadMessage}>
Pengecekan ganda
</p>
<p style={styles.description}>
Nyalakan agar kasir memeriksa kembali ketersediaan produk sebelum pelanggan membayar.
</p>
<Switch
onChange={(checked) => setIsNeedConfirmationState(checked ? 1 : 0)}
checked={isNeedConfirmationState === 1} // Convert to boolean
offColor="#888"
onColor="#4CAF50"
uncheckedIcon={false}
checkedIcon={false}
height={25}
width={50}
/>
<div className={styles.section}>
<div className={styles.sectionHeader}>
<div>
<div className={styles.sectionTitle}>Open bill</div>
<div className={styles.sectionDesc}>Izinkan pelanggan menambah pesanan dalam satu sesi dan bayar di akhir.</div>
</div>
<Switch
onChange={(checked) => setIsOpenBillAvailable(checked ? 1 : 0)}
checked={isOpenBillAvailable === 1}
offColor="#888"
onColor="#4CAF50"
uncheckedIcon={false}
checkedIcon={false}
height={25}
width={50}
/>
</div>
</div>
<div style={styles.buttonContainer}>
<button onClick={handleSave} style={styles.saveButton}>
Simpan
<div className={styles.section}>
<div className={styles.sectionHeader}>
<div>
<div className={styles.sectionTitle}>Pengecekan ganda</div>
<div className={styles.sectionDesc}>Kasir memeriksa kembali ketersediaan item sebelum pembayaran.</div>
</div>
<Switch
onChange={(checked) => setIsNeedConfirmationState(checked ? 1 : 0)}
checked={isNeedConfirmationState === 1}
offColor="#888"
onColor="#4CAF50"
uncheckedIcon={false}
checkedIcon={false}
height={25}
width={50}
/>
</div>
</div>
<div className={styles.footer}>
<div>
{saveStatus === 'success' && <span className={`${styles.banner} ${styles.bannerSuccess}`}>Simpan berhasil</span>}
{saveStatus === 'error' && <span className={`${styles.banner} ${styles.bannerError}`}>Gagal menyimpan</span>}
</div>
<button className={`${styles.button} ${styles.primary}`} onClick={handleSave} disabled={saving}>
{saving ? 'Menyimpan…' : 'Simpan'}
</button>
</div>
</div>
);
};
// Styles
const styles = {
container: {
position: 'relative',
overflowY: 'auto',
overflowX: 'hidden',
maxHeight: '80vh',
width: '100%',
backgroundColor: "white",
padding: "20px",
borderRadius: "8px",
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.1)",
textAlign: "center", // Center text and children
},
title: {
marginBottom: "20px",
fontWeight: "bold",
},
qrCodeContainer: {
backgroundColor: '#999999',
borderRadius: '20px',
position: "relative",
width: "100%",
height: "200px",
backgroundSize: "contain",
overflow: "hidden",
margin: "0 auto", // Center the QR code container
marginTop: '10px'
},
uploadMessage: {
fontWeight: 600,
textAlign: "left",
},
qrisConfigButton: {
borderRadius: '15px',
backgroundColor: '#28a745',
width: '144px',
textAlign: 'center',
color: 'white',
lineHeight: '24px',
marginLeft: '14px',
},
uploadButton: {
paddingRight: '10px',
backgroundColor: '#28a745',
borderRadius: '30px',
color: 'white',
fontWeight: 700,
height: '36px',
lineHeight: '36px',
paddingLeft: '10px',
paddingHeight: '10px',
},
resultMessage: {
marginTop: "-24px",
textAlign: "left",
display: 'flex',
justifyContent: 'space-between'
},
buttonContainer: {
marginTop: "20px",
textAlign: "left",
},
saveButton: {
padding: "10px 20px",
fontSize: "16px",
backgroundColor: "#28a745",
color: "#fff",
border: "none",
borderRadius: "30px",
cursor: "pointer",
transition: "background-color 0.3s",
},
switchContainer: {
textAlign: "left",
},
description: {
margin: "10px 0",
fontSize: "14px",
color: "#666",
},
sliderContainer: {
marginBottom: "20px",
},
label: {
display: "block",
marginBottom: "10px",
},
sliderWrapper: {
display: "flex",
alignItems: "center",
},
input: {
flex: "1",
margin: "0 10px",
},
};
export default SetPaymentQr;

View File

@@ -0,0 +1,171 @@
/* PaymentOptions.module.css */
.container {
position: relative;
overflow-y: auto;
overflow-x: hidden;
max-height: 80vh;
width: 100%;
background: #fff;
padding: 20px;
border-radius: 8px;
box-sizing: border-box;
}
.title {
margin: 0 0 16px 0;
font-size: 18px;
font-weight: 700;
}
.section {
background: #fafafa;
border: 1px solid #e6e6e6;
border-radius: 12px;
padding: 16px;
margin-bottom: 14px;
}
.sectionHeader {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.sectionTitle {
font-weight: 700;
font-size: 15px;
margin-bottom: 4px;
}
.sectionDesc {
font-size: 13px;
color: #666;
}
.row {
display: flex;
align-items: center;
gap: 12px;
margin-top: 10px;
}
.button {
padding: 10px 14px;
border-radius: 10px;
border: 1px solid #ddd;
background: #fff;
cursor: pointer;
}
.button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.primary {
background: #28a745;
border: none;
color: #fff;
}
.configButton {
font-weight: 600;
}
.imageBox {
background-color: #999999;
border-radius: 12px;
position: relative;
width: 100%;
height: 220px;
background-repeat: no-repeat;
background-position: center;
background-size: contain;
overflow: hidden;
margin: 10px 0 6px 0;
}
.smallNote {
font-size: 12px;
color: #777;
}
.detectRow {
margin-top: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
.tag {
padding: 6px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
}
.tagOk {
color: #155724;
background: #d4edda;
border: 1px solid #c3e6cb;
}
.tagBad {
color: #721c24;
background: #f8d7da;
border: 1px solid #f5c6cb;
}
.copyRow {
display: flex;
gap: 8px;
align-items: center;
margin-top: 8px;
}
.linkField {
flex: 1;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 10px;
font-size: 14px;
}
.actionsRight {
display: flex;
justify-content: flex-end;
margin-top: 8px;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 14px;
}
.banner {
padding: 8px 12px;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
}
.bannerSuccess {
color: #155724;
background: #d4edda;
border: 1px solid #c3e6cb;
}
.bannerError {
color: #721c24;
background: #f8d7da;
border: 1px solid #f5c6cb;
}
@media (max-width: 720px) {
.container { padding: 16px; }
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
.watermark {
z-index: 5;
z-index: 0; /* align with item lister */
margin-top: 30px;
background-color: rgb(222, 237, 100);
font-weight: 700;
@@ -28,7 +28,7 @@
/* Media query for desktop */
@media (min-width: 768px) { /* Adjust the min-width as needed */
.watermark {
z-index: 5;
z-index: 0; /* align with item lister */
margin-top: 30px;
background-color: rgb(222, 237, 100);
font-weight: 700;

View File

@@ -106,10 +106,10 @@ export async function getCafeByIdentifier(cafeIdentifyName) {
return -1;
}
}
export async function getOwnedCafes(userId) {
export async function getOwnedCafes(user_id) {
try {
const response = await fetch(
`${API_BASE_URL}/cafe/get-cafe-by-ownerId/` + userId,
`${API_BASE_URL}/cafe/get-cafe-by-ownerId/` + user_id,
{
method: "POST",
headers: {

View File

@@ -5,9 +5,95 @@ body {
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
background-color: #e9e9e9;
position: relative;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
/* Ensure proper scrolling behavior */
html, body {
width: 100%;
min-height: 100%;
overflow-x: hidden;
overflow-y: auto;
}
#root {
width: 100%;
min-height: 100%;
overflow: visible;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* Ensure proper z-index stacking */
* {
z-index: auto;
}
/* Responsive design adjustments */
@media (max-width: 768px) {
body {
font-size: 14px;
}
.container {
max-height: 75vh; /* Menaikkan tinggi maksimum pada mobile */
}
.materialCard {
margin-bottom: 12px;
}
.materialSummary {
padding: 14px 16px;
}
.materialName {
font-size: 15px;
}
.materialStock {
font-size: 13px;
margin-right: 12px;
}
.detailGrid {
grid-template-columns: 1fr;
gap: 12px;
}
.formGrid {
grid-template-columns: 1fr;
gap: 16px;
}
/* Memastikan MaterialList tetap dapat di-scroll */
.MaterialList-module__container {
max-height: 80vh;
overflow-y: auto;
}
}

View File

@@ -1,6 +1,6 @@
// src/CafePage.js
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useRef } from "react";
import {
useParams,
useSearchParams,
@@ -55,6 +55,7 @@ function CafePage({
const [searchParams] = useSearchParams();
const token = searchParams.get("token");
const { shopIdentifier, tableCode } = useParams();
// Send params to parent immediately (original behavior)
sendParam({ shopIdentifier, tableCode });
const {
@@ -76,18 +77,63 @@ function CafePage({
const [config, setConfig] = useState({});
const [beingEditedType, setBeingEditedType] = useState(0);
// Sticky floating cart bar animations and visibility
const [cartBump, setCartBump] = useState(false);
const [barIntro, setBarIntro] = useState(false);
const [historyPulse, setHistoryPulse] = useState(false);
const prevShowRef = useRef(false);
const prevTxnRef = useRef(null);
const showBar = !isEditMode && (user.username || cartItemsLength > 0);
const checkWelcomePageConfig = () => {
const parsedConfig = JSON.parse(welcomePageConfig);
if (parsedConfig.isWelcomePageActive == "true") {
const clicked = sessionStorage.getItem("getStartedClicked");
if (!clicked) {
sessionStorage.setItem("getStartedClicked", true);
document.body.style.overflow = "hidden";
setIsStarted(true);
}
useEffect(() => {
if (cartItemsLength > 0) {
setCartBump(true);
const t = setTimeout(() => setCartBump(false), 450);
return () => clearTimeout(t);
}
};
}, [cartItemsLength]);
useEffect(() => {
if (showBar && !prevShowRef.current) {
setBarIntro(true);
const t = setTimeout(() => setBarIntro(false), 500);
prevShowRef.current = true;
return () => clearTimeout(t);
}
if (!showBar) prevShowRef.current = false;
}, [showBar]);
useEffect(() => {
if (lastTransaction && lastTransaction !== prevTxnRef.current) {
setHistoryPulse(true);
const t = setTimeout(() => setHistoryPulse(false), 900);
prevTxnRef.current = lastTransaction;
return () => clearTimeout(t);
}
}, [lastTransaction]);
// const checkWelcomePageConfig = () => {
// const parsedConfig = JSON.parse(welcomePageConfig);
// if (parsedConfig.isWelcomePageActive == "true") {
// const clicked = sessionStorage.getItem("getStartedClicked");
// if (!clicked) {
// sessionStorage.setItem("getStartedClicked", true);
// document.body.style.overflow = "hidden";
// setIsStarted(true);
// }
// }
// };
useEffect(() => {
if (window.gtag && shopIdentifier) {
window.gtag('event', 'page_view', {
page_title: `Cafe - ${shopIdentifier}`,
page_location: window.location.href,
page_path: `/` + shopIdentifier,
shop_id: shopId || null, // opsional jika kamu mau track ID juga
});
}
}, [shopIdentifier]);
useEffect(() => {
if (welcomePageConfig) {
@@ -100,16 +146,16 @@ function CafePage({
isActive: parsedConfig.isWelcomePageActive === "true",
});
}
checkWelcomePageConfig();
// checkWelcomePageConfig();
}, [welcomePageConfig]);
useEffect(() => {
function fetchData() {
console.log(user.userId == shopOwnerId)
console.log(user.user_id == shopOwnerId)
setModal("create_item");
}
console.log(getLocalStorage('auth'))
if (getLocalStorage("auth") != null) {
const executeFetch = async () => {
@@ -118,7 +164,7 @@ function CafePage({
}
console.log(user)
console.log('open')
if (user.length != 0 && user.userId == shopOwnerId && shopItems.length == 0) fetchData();
if (user.length != 0 && user.user_id == shopOwnerId && shopItems.length == 0) fetchData();
};
executeFetch();
}
@@ -234,6 +280,8 @@ function CafePage({
removeConnectedGuestSides={removeConnectedGuestSides}
setIsEditMode={(e) => setIsEditMode(e)}
isEditMode={isEditMode}
zIndexLevel={9000}
rectZIndex={9000}
/>
<MusicPlayer
socket={socket}
@@ -307,29 +355,33 @@ function CafePage({
}
/>
))}
{!isEditMode && (user.username || cartItemsLength > 0) &&
<div style={{ marginTop: '10px', height: '40px', position: 'sticky', bottom: '40px', display: 'flex', justifyContent: 'center', alignItems: 'center', textAlign: 'center' }}>
{showBar &&
<div className={`StickyCartBar${barIntro ? ' intro' : ''}`}>
{(lastTransaction != null || cartItemsLength > 0) &&
<div onClick={goToCart} style={{ backgroundColor: '#73a585', width: user.username ? '55vw' : '70vw', height: '40px', borderRadius: '30px', display: 'flex', justifyContent: 'space-between', padding: '0 20px' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignContent: 'center' }}>{lastTransaction != null && '+'}{cartItemsLength} item</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', width: '130px' }}>
<div role="button" tabIndex={0} aria-label="Buka keranjang" onKeyDown={(e)=>{ if(e.key==='Enter'||e.key===' ') { e.preventDefault(); goToCart(); } }} onClick={goToCart} className={`cartBtn${cartBump ? ' bump' : ''}`}>
<div className="summary">
{(lastTransaction != null) && <span>+</span>}
<span>{cartItemsLength} item</span>
</div>
<div className="summary" style={{ gap: 6 }}>
{((lastTransaction == null || lastTransaction?.payment_type != 'paylater')) ?
<span style={{ whiteSpace: 'nowrap' }}>Rp{totalPrice}</span>
<span className="value">{`Rp ${Number(totalPrice || 0).toLocaleString('id-ID')}`}</span>
:
<span style={{ whiteSpace: 'nowrap' }}>Open bill</span>
<span className="value">Open bill</span>
}
<div style={{ display: 'flex', alignItems: 'center', marginLeft: '5px', width: '20px' }}>
<div className="icon">
<svg viewBox="0 0 34 34" style={{ fill: 'white', marginTop: '4px' }}>
<path d="M9.79175 24.75C8.09591 24.75 6.72383 26.1375 6.72383 27.8333C6.72383 29.5292 8.09591 30.9167 9.79175 30.9167C11.4876 30.9167 12.8751 29.5292 12.8751 27.8333C12.8751 26.1375 11.4876 24.75 9.79175 24.75ZM0.541748 0.0833435V3.16668H3.62508L9.17508 14.8679L7.09383 18.645C6.84717 19.0767 6.70842 19.5854 6.70842 20.125C6.70842 21.8208 8.09591 23.2083 9.79175 23.2083H28.2917V20.125H10.4392C10.2234 20.125 10.0538 19.9554 10.0538 19.7396L10.1001 19.5546L11.4876 17.0417H22.973C24.1292 17.0417 25.1467 16.4096 25.6709 15.4538L31.1901 5.44834C31.3134 5.23251 31.3751 4.97043 31.3751 4.70834C31.3751 3.86043 30.6813 3.16668 29.8334 3.16668H7.03217L5.583 0.0833435H0.541748ZM25.2084 24.75C23.5126 24.75 22.1405 26.1375 22.1405 27.8333C22.1405 29.5292 23.5126 30.9167 25.2084 30.9167C26.9042 30.9167 28.2917 29.5292 28.2917 27.8333C28.2917 26.1375 26.9042 24.75 25.2084 24.75Z"></path>
</svg>
{cartItemsLength > 0 && <span className={`badge${cartBump ? ' pop' : ''}`}>{cartItemsLength > 9 ? '9+' : cartItemsLength}</span>}
</div>
</div>
</div>
}
{user.username &&
<div onClick={goToTransactions} style={{ backgroundColor: '#73a585', width: '15vw', height: '40px', borderRadius: '30px', display: 'flex', justifyContent: 'center', marginLeft: lastTransaction != null || cartItemsLength > 0 ? '6px' : '0px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '38px', marginRight: '5px' }}>
<div style={{ display: 'flex', alignItems: 'center', marginLeft: '5px', width: '20px' }}>
<div role="button" tabIndex={0} aria-label="Lihat riwayat" onKeyDown={(e)=>{ if(e.key==='Enter'||e.key===' ') { e.preventDefault(); goToTransactions(); } }} onClick={goToTransactions} className={`historyBtn${historyPulse ? ' pulse' : ''}`}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 38, marginRight: 6 }}>
<div className="icon">
<svg viewBox="0 0 512 512">
<g
transform="translate(0 460) scale(0.09 -0.09)"
@@ -377,3 +429,4 @@ function CafePage({
}
export default CafePage;

View File

@@ -1,5 +1,6 @@
import React, { useRef, useEffect, useState } from "react";
import styles from "./Invoice.module.css";
import cartStyles from "./CartPage.module.css";
import { useParams } from "react-router-dom"; // Changed from useSearchParams to useLocation
import { ThreeDots, ColorRing } from "react-loader-spinner";
@@ -272,6 +273,8 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
socketId
);
localStorage.removeItem('lastTransaction')
// Dispatch the custom event
window.dispatchEvent(new Event("localStorageUpdated"));
}
else
@@ -382,7 +385,13 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
return (
<div className={styles.Invoice} style={{ height: (getItemsByCafeId(shopId).length > 0 ? '' : '100vh'), minHeight: (getItemsByCafeId(shopId).length > 0 ? '100vh' : '') }}>
<div onClick={goToShop} style={{ marginLeft: '22px', marginTop: '49px', marginRight: '10px', display: 'flex', flexWrap: 'nowrap', alignItems: 'center', fontSize: '25px' }} ><svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 512 512"><path d="M48,256c0,114.87,93.13,208,208,208s208-93.13,208-208S370.87,48,256,48,48,141.13,48,256Zm212.65-91.36a16,16,0,0,1,.09,22.63L208.42,240H342a16,16,0,0,1,0,32H208.42l52.32,52.73A16,16,0,1,1,238,347.27l-79.39-80a16,16,0,0,1,0-22.54l79.39-80A16,16,0,0,1,260.65,164.64Z" /></svg>Keranjang</div>
<div className={cartStyles.header}>
<div className={cartStyles.backBtn} onClick={goToShop} aria-label="Kembali">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 512 512"><path fill="#fff" d="M48,256c0,114.87,93.13,208,208,208s208-93.13,208-208S370.87,48,256,48,48,141.13,48,256Zm212.65-91.36a16,16,0,0,1,.09,22.63L208.42,240H342a16,16,0,0,1,0,32H208.42l52.32,52.73A16,16,0,1,1,238,347.27l-79.39-80a16,16,0,0,1,0-22.54l79.39-80A16,16,0,0,1,260.65,164.64Z" /></svg>
</div>
<div className={cartStyles.title}>Keranjang</div>
</div>
<div className={cartStyles.container}>
{(transactionData == null && getItemsByCafeId(shopId).length < 1) ?
<div style={{ height: '75vh', display: 'flex', justifyContent: 'center', flexDirection: 'column', alignContent: 'center', alignItems: 'center' }}>
@@ -441,21 +450,21 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
</div>
)}
<div className={styles.NoteContainer}>
<span>Catatan :</span>
<span></span>
</div>
<div className={styles.NoteContainer}>
<textarea
ref={textareaRef}
className={styles.NoteInput}
placeholder="Tambahkan catatan..."
/>
</div>
</div>
}
{getItemsByCafeId(shopId).length > 0 && (
<div className={`${styles.RoundedRectangle} ${cartStyles.sectionCard}`}>
<div className={cartStyles.sectionTitle}>Catatan Untuk Kasir</div>
<div className={cartStyles.divider}></div>
<textarea
ref={textareaRef}
className={styles.NoteInput}
placeholder="Contoh: tanpa gula, ekstra es, dsb."
/>
</div>
)}
{transactionData &&
<div className={styles.RoundedRectangle} style={{ backgroundColor: '#c3c3c3', fontSize: '15px', display: 'flex', justifyContent: 'space-between' }}>
{transactionData.payment_type != 'paylater' ?
@@ -573,6 +582,7 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
</>
)
}
</div>
</div>
);
}

View File

@@ -22,6 +22,7 @@
.CheckoutContainer {
bottom: 0px;
position: fixed;
z-index: 100; /* Menurunkan z-index agar tidak menutupi material list */
}
.EmailContainer {

View File

@@ -0,0 +1,68 @@
.header {
position: sticky;
top: 0;
z-index: 5;
background: var(--brand-sage, #6B8F71);
color: #fff;
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.backBtn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 999px;
background: rgba(255,255,255,0.15);
border: 1px solid rgba(255,255,255,0.25);
cursor: pointer;
}
.title {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 700;
font-size: 18px;
}
.container {
padding: 12px;
}
.sectionCard {
background: #fff;
border-radius: 14px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
padding: 12px;
margin: 12px 8px;
}
.sectionTitle {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 700;
font-size: 16px;
color: #2d2d2d;
margin: 4px 0 10px 4px;
}
.rowBetween {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.hint {
font-size: 12px;
color: #666;
}
.divider {
height: 1px;
background: #eee;
margin: 8px 0 12px 0;
}

View File

@@ -1,119 +1,153 @@
import React, { useState } from 'react';
import { createClerks } from '../helpers/userHelpers'; // Adjust the import path as needed
import React, { useEffect, useMemo, useState } from 'react';
import { createClerks, getClerks } from '../helpers/userHelpers';
import { useLocation } from "react-router-dom";
import styles from './CreateClerk.module.css';
const CreateClerk = ({ shopId }) => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [saving, setSaving] = useState(false);
const [banner, setBanner] = useState(null); // { type: 'success'|'error', text: string }
const [clerks, setClerks] = useState([]);
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const cafeIdParam = queryParams.get("cafeId");
const effectiveShopId = useMemo(()=> shopId || cafeIdParam, [shopId, cafeIdParam]);
useEffect(()=>{
const load = async ()=>{
if (!effectiveShopId) return;
try {
const data = await getClerks(effectiveShopId);
if (data && Array.isArray(data)) setClerks(data);
} catch (e) {}
};
load();
}, [effectiveShopId]);
const generatePassword = () => {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%';
let pwd = '';
for (let i = 0; i < 12; i++) pwd += chars[Math.floor(Math.random()*chars.length)];
setPassword(pwd);
};
const handleSubmit = async (event) => {
event.preventDefault();
setLoading(true);
setMessage('');
setSaving(true);
setBanner(null);
// Basic validation
if (!username || !password) {
setMessage('Username and password are required');
setLoading(false);
setBanner({ type: 'error', text: 'Username dan password wajib diisi' });
setSaving(false);
return;
}
if (username.length < 3) {
setBanner({ type: 'error', text: 'Username minimal 3 karakter' });
setSaving(false);
return;
}
if (password.length < 6) {
setBanner({ type: 'error', text: 'Password minimal 6 karakter' });
setSaving(false);
return;
}
try {
const create = await createClerks(shopId || cafeIdParam, username, password);
if (create) setMessage('Clerk created successfully');
else setMessage('Failed to create clerk');
const create = await createClerks(effectiveShopId, username, password);
if (create) {
setBanner({ type: 'success', text: 'Kasir berhasil ditambahkan' });
// Refresh list
try {
const data = await getClerks(effectiveShopId);
if (data && Array.isArray(data)) setClerks(data);
} catch {}
// Clear form
setUsername('');
setPassword('');
} else {
setBanner({ type: 'error', text: 'Gagal menambahkan kasir' });
}
} catch (error) {
setMessage('Error creating clerk');
setBanner({ type: 'error', text: 'Terjadi kesalahan saat menambahkan kasir' });
} finally {
setLoading(false);
setSaving(false);
}
};
return (
<div style={styles.container}>
<h2 style={styles.header}>Tambah Kasir</h2>
<form onSubmit={handleSubmit} style={styles.form}>
<input
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
style={styles.input}
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
style={styles.input}
/>
<button type="submit" style={styles.button} disabled={loading}>
{loading ? 'Creating...' : 'Create Clerk'}
</button>
{message && (
<p style={{ ...styles.message, color: message.includes('success') ? 'green' : 'red' }}>
{message}
</p>
<div className={styles.container}>
<div className={styles.header}>
<h2 className={styles.title}>Tambah Kasir</h2>
{banner && (
<div className={`${styles.banner} ${banner.type === 'success' ? styles.bannerSuccess : styles.bannerError}`}>
{banner.text}
</div>
)}
</form>
</div>
<div className={styles.section}>
<div className={styles.sectionTitle}>Form kasir baru</div>
<form onSubmit={handleSubmit}>
<div className={styles.formRow}>
<div className={styles.field}>
<label className={styles.label}>Username</label>
<input
className={styles.input}
type="text"
placeholder="kasir_baru"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className={styles.field}>
<label className={styles.label}>Password</label>
<div className={styles.pwdRow}>
<input
className={styles.input}
type={showPassword ? 'text' : 'password'}
placeholder="••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="button" className={styles.button} onClick={()=>setShowPassword(!showPassword)}>
{showPassword ? 'Sembunyikan' : 'Tampilkan'}
</button>
<button type="button" className={styles.button} onClick={generatePassword}>
Generate
</button>
</div>
</div>
</div>
<div className={styles.footer}>
<button className={`${styles.button} ${styles.primary}`} type="submit" disabled={saving}>
{saving ? 'Menambahkan…' : 'Tambah Kasir'}
</button>
</div>
</form>
</div>
<div className={styles.section}>
<div className={styles.sectionTitle}>Daftar kasir</div>
<div className={styles.list}>
{clerks && clerks.length > 0 ? (
clerks.map((c) => (
<div key={c.user_id || c.username} className={`${styles.listItem} ${styles.muted}`}>
<span>@{c.username}</span>
{/* Tempatkan tombol hapus jika API tersedia */}
</div>
))
) : (
<div className={styles.listItem}>Belum ada kasir</div>
)}
</div>
</div>
</div>
);
};
// Basic styling to make it mobile-friendly with a white background
const styles = {
container: {
backgroundColor: '#fff',
width: '100%',
maxWidth: '350px',
margin: '0 auto',
padding: '20px',
boxShadow: '0 4px 10px rgba(0, 0, 0, 0.1)',
borderRadius: '8px',
boxSizing: 'border-box',
},
header: {
textAlign: 'center',
marginBottom: '20px',
fontSize: '20px',
color: '#333',
},
form: {
display: 'flex',
flexDirection: 'column',
gap: '15px',
},
input: {
padding: '12px',
fontSize: '16px',
borderRadius: '8px',
border: '1px solid #ccc',
width: '100%',
boxSizing: 'border-box',
backgroundColor: '#f9f9f9',
},
button: {
padding: '12px',
fontSize: '16px',
borderRadius: '8px',
border: 'none',
backgroundColor: '#28a745',
color: 'white',
cursor: 'pointer',
width: '100%',
},
message: {
textAlign: 'center',
marginTop: '10px',
},
};
export default CreateClerk;

View File

@@ -0,0 +1,137 @@
/* CreateClerk.module.css */
.container {
background: #fff;
width: 100%;
max-width: 720px;
margin: 0 auto;
padding: 20px;
border-radius: 12px;
box-sizing: border-box;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.title {
font-size: 18px;
font-weight: 700;
margin: 0;
}
.section {
background: #fafafa;
border: 1px solid #e6e6e6;
border-radius: 12px;
padding: 16px;
margin-bottom: 14px;
}
.sectionTitle {
margin: 0 0 10px 0;
font-weight: 700;
font-size: 15px;
}
.formRow {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
.label {
font-size: 13px;
color: #555;
}
.input {
padding: 12px;
font-size: 14px;
border-radius: 10px;
border: 1px solid #ddd;
background: #fff;
}
.pwdRow {
display: flex;
gap: 8px;
}
.button {
padding: 10px 14px;
border-radius: 10px;
border: 1px solid #ddd;
background: #fff;
cursor: pointer;
}
.button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.primary {
background: #28a745;
border: none;
color: #fff;
}
.footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.banner {
padding: 8px 12px;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
}
.bannerSuccess {
color: #155724;
background: #d4edda;
border: 1px solid #c3e6cb;
}
.bannerError {
color: #721c24;
background: #f8d7da;
border: 1px solid #f5c6cb;
}
.list {
max-height: 220px;
overflow: auto;
border: 1px solid #e6e6e6;
border-radius: 8px;
padding: 8px;
}
.listItem {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 10px;
border-radius: 6px;
}
.muted {
background: #f0f0f0;
}
@media (max-width: 720px) {
.formRow { grid-template-columns: 1fr; }
}

View File

@@ -49,7 +49,7 @@ const Dashboard = ({ user, setModal }) => {
// Create admin functionality
createCafeOwner(newItem.email, newItem.username, newItem.password)
.then((newitem) => {
setItems([...items, { userId: newitem.userId, name: newitem.username }]);
setItems([...items, { user_id: newitem.user_id, name: newitem.username }]);
setIsCreating(false);
setNewItem({ name: "", type: "" });
})

View File

@@ -71,9 +71,9 @@ const LinktreePage = ({ user, setModal }) => {
// Handle manual coupon code check
const handleGetkCoupons = async () => {
const result = await getUserCoupons();
setCoupons(result.coupons);
console.log(result)
// const result = await getUserCoupons();
// setCoupons(result.coupons);
// console.log(result)
};
// Handle user transactions
@@ -95,24 +95,20 @@ const LinktreePage = ({ user, setModal }) => {
}
};
// Handle login
const handleLogin = async () => {
try {
setError(false);
setLoading(true);
const response = await loginUser(username, password);
if (response.success) {
localStorage.setItem('auth', response.token);
console.log(response)
window.location.href = response.cafeIdentifyName ? `/${response.cafeIdentifyName}` : '/';
} else {
setError(true);
}
} catch (error) {
setError(true);
} finally {
setLoading(false);
}
const handleLogin = () => {
const baseUrl = "https://kediritechnopark.com/";
const modal = "product";
const productId = 1;
const authorizedUri = "http://localhost:3000?token=";
const unauthorizedUri = `${baseUrl}?modal=${modal}&product_id=${productId}`;
const url =
`${baseUrl}?modal=${modal}&product_id=${productId}` +
`&authorized_uri=${encodeURIComponent(authorizedUri)}` +
`&unauthorized_uri=${encodeURIComponent(unauthorizedUri)}`;
window.location.href = url;
};
// Handle logout
@@ -152,7 +148,7 @@ const LinktreePage = ({ user, setModal }) => {
try {
if (user.roleId < 1) {
const newOwner = await createCafeOwner(newItem.email, newItem.username, newItem.password);
setItems([...items, { userId: newOwner.userId, name: newOwner.username }]);
setItems([...items, { user_id: newOwner.user_id, name: newOwner.username }]);
} else {
const newCafe = await createCafe(newItem.name);
setItems([...items, { cafeId: newCafe.cafeId, name: newCafe.name }]);
@@ -202,7 +198,7 @@ const LinktreePage = ({ user, setModal }) => {
];
console.log(items)
const selectedItems = items?.items?.find(item => (item.userId || item.cafeId) === selectedItemId);
const selectedItems = items?.items?.find(item => (item.user_id || item.cafeId) === selectedItemId);
// If the selected tenant is found, extract the cafes
const selectedSubItems = selectedItems?.subItems || [];
@@ -278,7 +274,7 @@ const LinktreePage = ({ user, setModal }) => {
))}
</div>
</div>
gratis 3 bulan pertama
gratis 1 bulan pertama
</div>
:
<div className={styles.mainHeading}>
@@ -290,57 +286,20 @@ const LinktreePage = ({ user, setModal }) => {
))}
</div>
</div>
Gratis 3 bulan pertama
Gratis 1 bulan pertama
</div>
}
<div className={styles.subHeading}>
Solusi berbasis web untuk memudahkan pengelolaan kedai, dengan fitur yang mempermudah pemilik, kasir, dan tamu berinteraksi.
</div>
{getLocalStorage('auth') == null && (
<div className={styles.LoginForm}>
<div className={`${styles.FormUsername} ${inputtingPassword ? styles.animateForm : wasInputtingPassword ? styles.reverseForm : ''}`}>
<label htmlFor="username" className={styles.usernameLabel}>---- Masuk -----------------------------</label>
<input
id="username"
placeholder="username"
maxLength="30"
className={!error ? styles.usernameInput : styles.usernameInputError}
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<button onClick={() => { setInputtingPassword(true); setWasInputtingPassword(true) }} className={styles.claimButton}>
<span></span>
</button>
</div>
<div className={`${styles.FormPassword} ${inputtingPassword ? styles.animateForm : wasInputtingPassword ? styles.reverseForm : styles.idleForm}`}>
<span>
<label onClick={() => setInputtingPassword(false)} htmlFor="password" className={styles.usernameLabel}> &lt;--- &lt;-- Kembali </label>
<label htmlFor="password" className={styles.usernameLabel}> &nbsp; ----- &nbsp; </label>
<label onClick={() => setModal('reset-password', { username: username })} className={styles.usernameLabel}>
lupa password?
</label>
</span>
<input
id="password"
placeholder="password"
type="password"
maxLength="30"
className={!error ? styles.usernameInput : styles.usernameInputError}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button
onClick={handleLogin}
className={`${styles.claimButton} ${loading ? styles.loading : ''}`}
disabled={loading}
>
<span>{loading ? 'Loading...' : 'Masuk'}</span>
<button onClick={() => handleLogin()} className={styles.claimButton}>
<span>Masuk</span>
</button>
</div>
</div>
)}
<div className={styles.footer}>
<div className={styles.footerLinks}>

View File

@@ -7,6 +7,8 @@
font-size: calc(10px + 2vmin);
color: rgba(88, 55, 50, 1);
background-color: #e9e9e9;
position: relative;
z-index: 50; /* Memastikan berada di bawah material list */
}
.Invoice-title {
@@ -45,6 +47,8 @@
bottom: 0;
right: 0;
left: 0;
z-index: 100; /* Menurunkan z-index lebih jauh agar tidak menutupi material list */
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
}
.PaymentOptionMargin {
@@ -159,6 +163,7 @@
padding: 10px 0;
margin-bottom: 7px;
}
.OrderTypeContainer {
display: flex;
justify-content: space-between;

View File

@@ -110,7 +110,7 @@ const LinktreePage = ({ data, setModal }) => {
</div>
<div className={styles.linktreeForm}>
<button onClick={()=>window.open("https://api.whatsapp.com/send?phone=6281318894994&text=Saya%20ingin%20coba%20gratis%203%20bulan")} className={styles.claimButton}>
<span>Dapatkan voucher gratis 3 bulan</span>
<span>Dapatkan voucher gratis 1 bulan</span>
</button>
</div>
<div className={styles.footer}>

View File

@@ -338,7 +338,6 @@
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-top: 2rem;
}
.footerLinks {

View File

@@ -1,12 +1,4 @@
import React, { useState, useRef, useEffect } from "react";
import jsQR from "jsqr";
import { getImageUrl } from "../helpers/itemHelper";
import {
getCafe,
saveCafeDetails,
setConfirmationStatus,
} from "../helpers/cafeHelpers";
import React, { useState, useEffect } from "react";
import {
getMaterials,
createMaterial,
@@ -17,347 +9,497 @@ import {
getMaterialMutations,
} from "../helpers/materialMutationHelpers";
import Switch from "react-switch"; // Import the Switch component
import Carousel from '../components/Carousel'
import styles from './MaterialList.module.css'; // Import the CSS Module
import styles from './MaterialList.module.css';
const SetPaymentQr = ({ cafeId }) => {
// All your state and logic goes here (unchanged)
const MaterialList = ({ cafeId }) => {
// State declarations
const [materials, setMaterials] = useState([]);
const [mutations, setMutations] = useState([]);
const [newMaterialName, setNewMaterialName] = useState("");
const [newMaterialUnit, setNewMaterialUnit] = useState("kilogram");
const [newMaterialImage, setNewMaterialImage] = useState(null);
const [deleting, setDeleting] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [showForm, setShowForm] = useState(false);
const [selectedMaterialIndex, setSelectedMaterialIndex] = useState(-1);
const [latestMutation, setLatestMutation] = useState([]);
const [currentQuantity, setCurrentQuantity] = useState(-1);
const [currentPrice, setCurrentPrice] = useState(0);
const [newPrice, setNewPrice] = useState(0);
const [quantityChange, setQuantityChange] = useState(0);
// Add material form state
const [showAddForm, setShowAddForm] = useState(false);
const [newMaterialName, setNewMaterialName] = useState("");
const [newMaterialUnit, setNewMaterialUnit] = useState("kilogram");
// Edit material state
const [editingMaterialId, setEditingMaterialId] = useState(null);
const [newQuantity, setNewQuantity] = useState(0);
const [newPrice, setNewPrice] = useState("");
// Expand state for each material
const [expandedMaterials, setExpandedMaterials] = useState({});
// View state
const [sortOrder, setSortOrder] = useState("desc");
const [isEditCurrentPrice, setIsEditCurrentPrice] = useState(false);
const [isViewingHistory, setIsViewingHistory] = useState(false);
const [isViewingHistory, setIsViewingHistory] = useState({});
// Format currency helper
const formatCurrency = (value) => {
if (!value) return "0";
return parseInt(value.toString().replace(/\./g, "")).toLocaleString('id-ID');
};
const convertToInteger = (formattedValue) => {
// Remove dots and convert to integer
return parseInt(formattedValue.replace(/\./g, ""), 10);
return parseInt(formattedValue.replace(/\./g, ""), 10) || 0;
};
const formatCurrency = (value) => {
if (!value) return "";
// Remove existing formatting (dots) and format again
const numericValue = value.toString().replace(/\D/g, ""); // Keep only digits
return numericValue.replace(/\B(?=(\d{3})+(?!\d))/g, "."); // Add dot as thousands separator
};
const handleChange = (e) => {
// Handle price input change
const handlePriceChange = (e) => {
const formattedValue = formatCurrency(e.target.value);
setNewPrice(formattedValue);
};
// Fetch materials and mutations
useEffect(() => {
const fetchMaterials = async () => {
const fetchData = async () => {
try {
const data = await getMaterials(cafeId);
setMaterials(data);
console.log(data)
setError(null);
if (data.length > 0) {
setSelectedMaterialIndex(0);
}
} catch (error) {
console.error("Error fetching materials:", error);
setError("Failed to fetch materials.");
}
};
const [materialsData, mutationsData] = await Promise.all([
getMaterials(cafeId),
getMaterialMutations(cafeId)
]);
const fetchMutations = async () => {
try {
const data = await getMaterialMutations(cafeId);
setMutations(data);
setMaterials(materialsData);
setMutations(mutationsData);
setError(null);
} catch (err) {
setError(err.message);
console.error("Error fetching data:", err);
setError("Gagal memuat data bahan baku.");
} finally {
setLoading(false);
}
};
fetchMaterials();
fetchMutations();
if (cafeId) {
fetchData();
}
}, [cafeId]);
const filteredMutations = mutations.filter((mutation) => mutation.materialId === materials[selectedMaterialIndex]?.materialId) || [];
// Handle create material
const handleCreateMaterial = async (e) => {
e.preventDefault();
if (!newMaterialName.trim()) return;
const sortedMutations = filteredMutations
.filter((mutation) => mutation.materialId === materials[selectedMaterialIndex].materialId)
.sort((a, b) => {
setLoading(true);
try {
const formData = new FormData();
formData.append("name", newMaterialName);
formData.append("unit", newMaterialUnit);
await createMaterial(cafeId, formData);
// Reset form
setNewMaterialName("");
setNewMaterialUnit("kilogram");
setShowAddForm(false);
// Refresh materials list
const data = await getMaterials(cafeId);
setMaterials(data);
setError(null);
} catch (err) {
console.error("Error creating material:", err);
setError("Gagal menambah bahan baku.");
} finally {
setLoading(false);
}
};
// Handle delete material
const handleDeleteMaterial = async (materialId) => {
if (!window.confirm("Apakah Anda yakin ingin menghapus bahan baku ini?")) return;
setLoading(true);
try {
await deleteMaterial(materialId);
// Refresh materials list
const data = await getMaterials(cafeId);
setMaterials(data);
// Close expanded view if deleted material was expanded
setExpandedMaterials(prevState => {
const newState = { ...prevState };
delete newState[materialId];
return newState;
});
setError(null);
} catch (err) {
console.error("Error deleting material:", err);
setError("Gagal menghapus bahan baku.");
} finally {
setLoading(false);
}
};
// Handle update stock
const handleUpdateStock = async (materialId) => {
if (newQuantity === 0 && !convertToInteger(newPrice)) return;
setLoading(true);
try {
const material = materials.find(m => m.materialId === materialId);
const currentQuantity = material ? material.currentStock : 0;
const finalQuantity = currentQuantity + newQuantity;
const formData = new FormData();
formData.append("newStock", finalQuantity);
formData.append("priceAtp", convertToInteger(newPrice));
formData.append("reason", `Update stok: ${newQuantity > 0 ? '+' : ''}${newQuantity}`);
await createMaterialMutation(materialId, formData);
// Refresh data
const [materialsData, mutationsData] = await Promise.all([
getMaterials(cafeId),
getMaterialMutations(cafeId)
]);
setMaterials(materialsData);
setMutations(mutationsData);
// Reset form
setNewQuantity(0);
setNewPrice("");
setEditingMaterialId(null);
setError(null);
} catch (err) {
console.error("Error updating stock:", err);
setError("Gagal memperbarui stok.");
} finally {
setLoading(false);
}
};
// Get filtered and sorted mutations for a material
const getFilteredMutations = (materialId) => {
const filtered = mutations.filter(m => m.materialId === materialId);
return filtered.sort((a, b) => {
if (sortOrder === "asc") {
return new Date(a.createdAt) - new Date(b.createdAt);
} else {
return new Date(b.createdAt) - new Date(a.createdAt);
}
});
const handleCreateMaterial = async (e) => {
e.preventDefault();
setLoading(true);
const formData = new FormData();
formData.append("name", newMaterialName);
formData.append("unit", newMaterialUnit);
if (newMaterialImage) {
formData.append("image", newMaterialImage);
}
try {
await createMaterial(cafeId, formData);
setNewMaterialName("");
setNewMaterialUnit("kilogram");
setNewMaterialImage(null);
setShowForm(false);
const data = await getMaterials(cafeId);
setMaterials(data);
setError(null);
if (data.length > 0) {
setSelectedMaterialIndex(0);
}
} catch (error) {
console.error("Error creating material:", error);
setError("Failed to create material.");
} finally {
setLoading(false);
}
};
const handleDeleteMaterial = async (materialId) => {
setDeleting(materialId);
try {
await deleteMaterial(materialId);
const updatedMaterials = materials.filter(
(material) => material.materialId !== materialId
);
setMaterials(updatedMaterials);
setError(null);
if (selectedMaterialIndex === materialId) {
setSelectedMaterialIndex(
updatedMaterials.length > 0 ? updatedMaterials[0].materialId : null
);
}
} catch (error) {
console.error("Error deleting material:", error);
setError("Failed to delete material.");
} finally {
setDeleting(null);
}
};
const handleQuantityChange = (change) => {
setQuantityChange((prev) => prev + change);
if (quantityChange + change < 1) setNewPrice(currentPrice);
setIsEditCurrentPrice(false);
};
useEffect(() => {
setQuantityChange(0);
if (materials.length > 0 || selectedMaterialIndex > -1) {
const materialMutations = mutations.filter(
(mutation) => mutation.materialId === materials[selectedMaterialIndex]?.materialId
);
console.log(materialMutations)
if (materialMutations.length > 0) {
const latestMutation = materialMutations.reduce(
(latest, current) =>
new Date(current.createdAt) > new Date(latest.createdAt)
? current
: latest,
materialMutations[0]
);
setLatestMutation(latestMutation);
setCurrentQuantity(latestMutation.newStock);
setCurrentPrice(formatCurrency(latestMutation.priceAtp));
setNewPrice(formatCurrency(latestMutation.priceAtp));
} else {
setCurrentQuantity(0); // Default value if no mutations exist
setLatestMutation({ newStock: 0 });
setCurrentPrice(0);
setNewPrice(0);
}
}
setIsViewingHistory(false);
}, [materials, mutations, selectedMaterialIndex]);
const handleUpdateStock = async () => {
setLoading(true);
try {
const newprice = convertToInteger(newPrice)
const newStock = currentQuantity + quantityChange;
const formData = new FormData();
formData.append("newStock", newStock);
formData.append("priceAtp", newprice);
formData.append("reason", "Stock update");
await createMaterialMutation(materials[selectedMaterialIndex].materialId, formData);
setQuantityChange(0);
const updatedMutations = await getMaterialMutations(cafeId);
setMutations(updatedMutations);
setCurrentQuantity(newStock);
setError(null);
} catch (error) {
console.error("Error updating stock:", error);
setError("Failed to update stock.");
} finally {
setLoading(false);
}
};
const currentMaterial = materials.find(
(material) => material.materialId === selectedMaterialIndex
);
// Format date for display
const formatDate = (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleString();
return date.toLocaleString('id-ID', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
// Toggle material expansion
const toggleExpand = (materialId) => {
setExpandedMaterials(prevState => {
const newState = {
...prevState,
[materialId]: !prevState[materialId]
};
// Reset form when expanding/collapsing
if (!prevState[materialId]) {
setNewQuantity(0);
setNewPrice("");
setEditingMaterialId(null);
}
return newState;
});
};
// Toggle history view for a material
const toggleHistoryView = (materialId) => {
setIsViewingHistory(prevState => ({
...prevState,
[materialId]: !prevState[materialId]
}));
};
// Handle quantity change
const handleQuantityChange = (change) => {
setNewQuantity(prev => Math.max(0, prev + change));
};
if (loading && materials.length === 0) {
return (
<div className={styles.container}>
<div className={styles.loading}>
<div className={styles.loadingSpinner}></div>
<div>Memuat data bahan baku...</div>
</div>
</div>
);
}
return (
<div className={styles.container}>
{loading ? (
<></>
) : (
<>
<h3 className={styles.title}>Bahan baku</h3>
<Carousel items={materials} onSelect={(e) => setSelectedMaterialIndex(e)} selectedIndex={selectedMaterialIndex} />
{selectedMaterialIndex !== -1 ? (
<>
<div className={styles.switchContainer}>
<h3>Stok sekarang {currentQuantity}</h3>
</div>
<div className={styles.header}>
<h2 className={styles.title}>Manajemen Bahan Baku</h2>
<button
className={styles.addButton}
onClick={() => setShowAddForm(!showAddForm)}
>
{showAddForm ? 'Batal' : '+ Tambah Bahan Baku'}
</button>
</div>
<div className={styles.stokContainer}>
<button onClick={() => handleQuantityChange(currentQuantity + quantityChange > 0 ? -1 : 0)} className={styles.stockButton}>
-
</button>
<p>{currentQuantity + quantityChange}</p>
<button onClick={() => handleQuantityChange(1)} className={styles.stockButton}>
+
</button>
</div>
<div className={styles.uploadMessage}>
<p>harga per {materials && materials[selectedMaterialIndex]?.unit} sekarang</p>
</div>
<div className={styles.resultMessage}>
<input
className={styles.resultMessageInput} // Replace inline style with CSS module class
disabled={!isEditCurrentPrice || quantityChange < 1}
value={newPrice}
onChange={handleChange}
placeholder="Enter amount"
/>
<div onClick={() => quantityChange < 1 ? null : setIsEditCurrentPrice(!isEditCurrentPrice)} className={quantityChange < 1 ? styles.changeButtonDisabled : styles.changeButtonEnabled}>
{isEditCurrentPrice ? 'Terapkan' : 'Ganti'}
<div className={styles.content}>
{error && (
<div className={styles.error} style={{
padding: '12px',
backgroundColor: '#fff0f0',
color: '#dc3545',
borderRadius: '8px',
marginBottom: '20px'
}}>
{error}
</div>
)}
{/* Add Material Form */}
{showAddForm && (
<div className={styles.addMaterialForm}>
<h3 className={styles.sectionTitle}>Tambah Bahan Baku Baru</h3>
<form onSubmit={handleCreateMaterial}>
<div className={styles.formGrid}>
<div className={styles.formGroup}>
<label className={styles.formLabel}>Nama Bahan Baku</label>
<input
type="text"
className={styles.formInput}
value={newMaterialName}
onChange={(e) => setNewMaterialName(e.target.value)}
placeholder="Masukkan nama bahan baku"
required
/>
</div>
<div className={styles.formGroup}>
<label className={styles.formLabel}>Satuan</label>
<select
className={styles.formInput}
value={newMaterialUnit}
onChange={(e) => setNewMaterialUnit(e.target.value)}
>
<option value="gram">gram</option>
<option value="ons">ons</option>
<option value="kilogram">kilogram</option>
<option value="kuintal">kuintal</option>
<option value="liter">liter</option>
<option value="piece">piece</option>
<option value="meter">meter</option>
<option value="pack">pack</option>
<option value="sachet">sachet</option>
<option value="box">box</option>
</select>
</div>
</div>
<div className={styles.buttonContainer}>
<button onClick={handleUpdateStock} className={styles.saveButton}>
Laporkan {quantityChange > 0 ? 'penambahan' : 'stok sekarang'} {quantityChange < 1 ? currentQuantity + quantityChange : quantityChange} {materials[selectedMaterialIndex]?.unit}
<div className={styles.formActions}>
<button
type="button"
className={styles.cancelButton}
onClick={() => {
setShowAddForm(false);
setNewMaterialName("");
setNewMaterialUnit("kilogram");
}}
>
Batal
</button>
<button
type="submit"
className={styles.saveButton}
disabled={!newMaterialName.trim()}
>
Tambah Bahan Baku
</button>
</div>
<div className={styles.historyTab}>
<h3 onClick={() => setIsViewingHistory(!isViewingHistory)}> {isViewingHistory ? '˅' : '˃'} Riwayat stok</h3>
{selectedMaterialIndex !== -1 && isViewingHistory && !loading && (
<>
<div className={styles.sorter} onClick={() => setSortOrder(sortOrder == 'asc' ? 'desc' : 'asc')}>
Urutkan: {sortOrder === 'asc' ? "terlama" : "terbaru"} <div style={{ transform: 'rotate(90deg)' }}>&lt;&gt;</div>
</div>
<div className={styles.historyContainer}>
<div className={styles.mutationContainer}>
{sortedMutations.length > 0 ? (
sortedMutations.map((mutation) => (
<div key={mutation.id} className={styles.mutationCard}>
<div style={{ width: '42px', backgroundColor: '#b9b9b9', borderRadius: '10px', padding: '3px', paddingBottom: '0' }}>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<g id="Interface / Book">
<path id="Vector" d="M5 19.5002V6.2002C5 5.08009 5 4.51962 5.21799 4.0918C5.40973 3.71547 5.71547 3.40973 6.0918 3.21799C6.51962 3 7.08009 3 8.2002 3H17.4002C17.9602 3 18.2407 3 18.4546 3.10899C18.6427 3.20487 18.7948 3.35774 18.8906 3.5459C18.9996 3.75981 19 4.04005 19 4.6001V16.4001C19 16.9601 18.9996 17.2398 18.8906 17.4537C18.7948 17.6419 18.6429 17.7952 18.4548 17.8911C18.2411 18 17.961 18 17.402 18H7.25C6.00736 18 5 19.0074 5 20.25C5 20.6642 5.33579 21 5.75 21H16.402C16.961 21 17.2411 21 17.4548 20.8911C17.6429 20.7952 17.7948 20.642 17.8906 20.4538C17.9996 20.2399 18 19.9601 18 19.4V18" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</g>
</g>
</svg>
</div>
<div className={styles.mutationTitle}>
<h4>{formatDate(mutation.createdAt)}</h4>
<p>Total stok: {mutation.newStock} || +{mutation.newStock - mutation.oldStock} || {formatCurrency((mutation.newStock - mutation.oldStock) * mutation.priceAtp)}</p>
</div>
</div>
))
) : (
<p>Tidak ada laporan perubahan stok.</p>
)}
</form>
</div>
)}
{/* Materials List */}
{materials.length === 0 ? (
<div className={styles.emptyState}>
<div className={styles.emptyStateIcon}>📭</div>
<div className={styles.emptyStateText}>Belum ada bahan baku yang terdaftar</div>
<button
className={styles.addButton}
onClick={() => setShowAddForm(true)}
>
+ Tambah Bahan Baku Pertama
</button>
</div>
) : (
<div className={styles.materialsList}>
{materials.map((material) => (
<div key={material.materialId} className={styles.materialCard}>
{/* Material Summary (Always Visible) */}
<div
className={styles.materialSummary}
onClick={() => toggleExpand(material.materialId)}
>
<h3 className={styles.materialName}>{material.name}</h3>
<div className={styles.materialStock}>
{material.currentStock} {material.unit}
</div>
<div className={`${styles.expandIcon} ${expandedMaterials[material.materialId] ? styles.expanded : ''}`}>
</div>
</div>
{/* Material Detail (Expandable) */}
{expandedMaterials[material.materialId] && (
<div className={styles.materialDetail}>
<div className={styles.detailGrid}>
<div className={styles.detailItem}>
<span className={styles.detailLabel}>Stok Saat Ini</span>
<span className={styles.detailValue}>{material.currentStock} {material.unit}</span>
</div>
<div className={styles.detailItem}>
<span className={styles.detailLabel}>Harga per {material.unit}</span>
<span className={styles.detailValue}>Rp {formatCurrency(material.currentPrice || 0)}</span>
</div>
</div>
</>
<div className={styles.detailActions}>
<button
className={styles.detailButton}
onClick={() => {
setEditingMaterialId(material.materialId);
}}
>
Update Stok
</button>
<button
className={styles.detailButton}
onClick={() => handleDeleteMaterial(material.materialId)}
>
🗑 Hapus
</button>
</div>
{/* Stock Update Form */}
{editingMaterialId === material.materialId && (
<div className={styles.stockUpdateForm}>
<h4 className={styles.formTitle}>Update Stok</h4>
<div className={styles.quantityControls}>
<button
className={styles.quantityButton}
onClick={() => handleQuantityChange(-1)}
disabled={newQuantity <= 0}
>
-
</button>
<div className={styles.quantityDisplay}>
{newQuantity > 0 ? `+${newQuantity}` : newQuantity}
</div>
<button
className={styles.quantityButton}
onClick={() => handleQuantityChange(1)}
>
+
</button>
</div>
<div className={styles.formGroup}>
<label className={styles.formLabel}>Harga per {material.unit}</label>
<input
type="text"
className={styles.formInput}
value={newPrice}
onChange={handlePriceChange}
placeholder="Masukkan harga"
/>
</div>
<div className={styles.formActions}>
<button
className={styles.cancelButton}
onClick={() => {
setEditingMaterialId(null);
setNewQuantity(0);
setNewPrice("");
}}
>
Batal
</button>
<button
className={styles.saveButton}
onClick={() => handleUpdateStock(material.materialId)}
disabled={newQuantity === 0 && !convertToInteger(newPrice)}
>
Simpan Perubahan
</button>
</div>
</div>
)}
{/* History Section */}
<div className={styles.historySection}>
<h4
className={styles.historyTitle}
onClick={() => toggleHistoryView(material.materialId)}
>
{isViewingHistory[material.materialId] ? '▲' : '▼'} Riwayat Stok
</h4>
{isViewingHistory[material.materialId] && (
<>
<div className={styles.sortControls}>
<button
className={styles.sortButton}
onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}
>
Urutkan: {sortOrder === 'asc' ? 'Terlama' : 'Terbaru'}
</button>
</div>
<div className={styles.historyItems}>
{getFilteredMutations(material.materialId).length > 0 ? (
getFilteredMutations(material.materialId).map((mutation) => (
<div key={mutation.id} className={styles.historyItem}>
<div className={styles.historyIcon}>📊</div>
<div className={styles.historyContent}>
<div className={styles.historyDate}>{formatDate(mutation.createdAt)}</div>
<div className={styles.historyDetails}>
Stok: {mutation.newStock} {material.unit} |
Perubahan: {mutation.newStock - mutation.oldStock > 0 ? '+' : ''}{mutation.newStock - mutation.oldStock} |
Harga: Rp {formatCurrency(mutation.priceAtp)}
</div>
</div>
</div>
))
) : (
<div className={styles.emptyState} style={{ padding: '20px' }}>
<div>Belum ada riwayat perubahan stok</div>
</div>
)}
</div>
</>
)}
</div>
</div>
)}
</div>
</>
) : (
<>
<div className={styles.description}>
<div style={{ marginRight: "5px", fontSize: "1.2em" }}></div>
<h6 style={{ margin: 0, textAlign: "left", fontSize: '12px' }}>
Fitur ini mempermudah mengelola biaya dan memantau pengeluaran bahan.
</h6>
</div>
<div className={styles.switchContainer}>
<h3>Buat bahan baru</h3>
</div>
<div className={styles.resultMessage}>
<input
className={styles.resultMessageInput}
value={newMaterialName}
onChange={(event) => setNewMaterialName(event.target.value)}
placeholder="Masukkan nama barang"
style={{width: '100%', height: '31px'}}
/>
</div>
<select
id="materialUnit"
value={newMaterialUnit}
onChange={(e) => setNewMaterialUnit(e.target.value)}
className={styles.unit}
style={{height: '37px'}}
>
<option value="gram">Satuan: gram</option>
<option value="ons">Satuan: ons</option>
<option value="kilogram">Satuan: kilogram</option>
<option value="kuintal">Satuan: kuintal</option>
<option value="liter">Satuan: liter</option>
<option value="piece">Satuan: piece</option>
<option value="meter">Satuan: meter</option>
<option value="pack">Satuan: pack</option>
<option value="sachet">Satuan: sachet</option>
<option value="box">Satuan: box</option>
</select>
<div className={styles.buttonContainer}>
<button className={styles.saveButton} onClick={handleCreateMaterial}>
Buat bahan baku
</button>
</div>
</>
)}
</>
)}
))}
</div>
)}
</div>
</div>
);
};
export default SetPaymentQr;
export default MaterialList;

View File

@@ -1,149 +1,556 @@
/* SetPaymentQr.module.css */
/* MaterialList.module.css */
.container {
width: 100%;
min-height: 47vh;
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
text-align: center;
width: 100%;
background-color: #ffffff;
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
padding: 0;
margin-bottom: 24px;
transition: all 0.3s ease;
position: relative;
z-index: 200; /* Menaikkan z-index agar berada di atas cartbar */
max-height: 70vh; /* Membatasi tinggi maksimum */
overflow-y: auto; /* Memungkinkan scrolling vertikal */
}
.container:hover {
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.header {
padding: 24px;
border-bottom: 1px solid #e6e6e6;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
background-color: #ffffff;
z-index: 201; /* Memastikan header berada di atas container */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02);
}
.title {
margin: 0;
font-weight: 700;
font-size: 24px;
color: #333;
}
.addButton {
background-color: #28a745;
color: white;
border: none;
border-radius: 12px;
padding: 12px 24px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 2px 6px rgba(40, 167, 69, 0.2);
position: relative;
z-index: 202; /* Memastikan tombol berada di atas header */
}
.addButton:hover {
background-color: #218838;
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(40, 167, 69, 0.3);
}
.addButton:disabled {
background-color: #a1a1a1;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.content {
padding: 24px;
position: relative;
z-index: 200;
}
/* Materials List Styles */
.materialsList {
display: flex;
flex-direction: column;
gap: 12px;
}
.materialCard {
background: #ffffff;
border-radius: 12px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
border: 1px solid #eee;
position: relative;
z-index: 200;
}
.materialCard:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: #28a745;
}
.materialSummary {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
cursor: pointer;
position: relative;
z-index: 201;
}
.materialName {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0;
flex: 1;
}
.materialStock {
font-size: 14px;
color: #666;
margin: 0 16px 0 0;
}
.expandIcon {
font-size: 18px;
color: #28a745;
transition: transform 0.2s ease;
}
.expandIcon.expanded {
transform: rotate(180deg);
}
/* Material Detail Styles */
.materialDetail {
padding: 0 20px 20px 20px;
border-top: 1px solid #eee;
background: #f9f9f9;
border-radius: 0 0 12px 12px;
position: relative;
z-index: 200;
}
.detailGrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 20px;
}
.detailItem {
background: #ffffff;
padding: 16px;
border-radius: 10px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.detailLabel {
font-size: 13px;
color: #666;
margin-bottom: 4px;
}
.detailValue {
font-size: 18px;
font-weight: 600;
color: #333;
}
.detailActions {
display: flex;
gap: 12px;
margin-top: 16px;
}
.detailButton {
flex: 1;
padding: 12px 16px;
border-radius: 10px;
border: none;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.updateButton {
background-color: #28a745;
color: white;
box-shadow: 0 2px 6px rgba(40, 167, 69, 0.2);
}
.updateButton:hover {
background-color: #218838;
box-shadow: 0 4px 10px rgba(40, 167, 69, 0.3);
}
.deleteButton {
background-color: #dc3545;
color: white;
box-shadow: 0 2px 6px rgba(220, 53, 69, 0.2);
}
.deleteButton:hover {
background-color: #c82333;
box-shadow: 0 4px 10px rgba(220, 53, 69, 0.3);
}
/* Stock Update Form */
.stockUpdateForm {
background: #ffffff;
border-radius: 12px;
padding: 20px;
margin-top: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
position: relative;
z-index: 201;
}
.formTitle {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0 0 20px 0;
}
.quantityControls {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 20px;
}
.quantityButton {
width: 40px;
height: 40px;
border-radius: 50%;
border: 1px solid #ddd;
background: #ffffff;
font-size: 20px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.quantityButton:hover {
border-color: #28a745;
background: #f0f8f1;
color: #28a745;
}
.quantityDisplay {
font-size: 20px;
font-weight: 600;
min-width: 80px;
text-align: center;
}
.formGroup {
margin-bottom: 20px;
}
.formLabel {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
font-size: 14px;
}
.formInput {
width: 100%;
padding: 12px 16px;
border-radius: 10px;
border: 1px solid #ddd;
font-size: 16px;
transition: all 0.2s ease;
background: #ffffff;
}
.formInput:focus {
outline: none;
border-color: #28a745;
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.1);
}
.formActions {
display: flex;
gap: 12px;
}
.formButton {
flex: 1;
padding: 14px 16px;
border-radius: 10px;
border: none;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.saveButton {
background-color: #28a745;
color: white;
box-shadow: 0 2px 6px rgba(40, 167, 69, 0.2);
}
.saveButton:hover {
background-color: #218838;
box-shadow: 0 4px 10px rgba(40, 167, 69, 0.3);
}
.cancelButton {
background-color: #f0f0f0;
color: #333;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
}
.cancelButton:hover {
background-color: #e0e0e0;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
/* Add Material Form */
.addMaterialForm {
background: #f8f9fa;
border-radius: 12px;
padding: 24px;
margin-top: 24px;
position: relative;
z-index: 201;
}
.formSection {
margin-bottom: 24px;
}
.sectionTitle {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0 0 20px 0;
padding-bottom: 12px;
border-bottom: 1px solid #e6e6e6;
}
.formGrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.fullWidth {
grid-column: 1 / -1;
}
/* Empty State */
.emptyState {
text-align: center;
padding: 60px 20px;
color: #999;
}
.emptyStateIcon {
font-size: 48px;
margin-bottom: 16px;
color: #ccc;
}
.emptyStateText {
font-size: 18px;
margin-bottom: 24px;
color: #666;
}
/* History Section */
.historySection {
margin-top: 24px;
border-top: 1px solid #e6e6e6;
padding-top: 20px;
}
.historyTitle {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0 0 16px 0;
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
}
.historyTitle:hover {
color: #28a745;
}
.historyItems {
display: flex;
flex-direction: column;
gap: 12px;
max-height: 250px;
overflow-y: auto;
padding-right: 8px;
}
.historyItems::-webkit-scrollbar {
width: 6px;
}
.historyItems::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.historyItems::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 10px;
}
.historyItem {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 16px;
background: #ffffff;
border-radius: 10px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.historyIcon {
width: 36px;
height: 36px;
background: #b9b9b9;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 16px;
}
.historyContent {
flex: 1;
}
.historyDate {
font-size: 13px;
font-weight: 600;
color: #333;
margin: 0 0 2px 0;
}
.historyDetails {
font-size: 13px;
color: #666;
margin: 0;
}
.sortControls {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
}
.sortButton {
background: #f0f0f0;
border: none;
border-radius: 20px;
padding: 6px 14px;
font-size: 13px;
color: #555;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 6px;
}
.sortButton:hover {
background: #e0e0e0;
}
/* Loading State */
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.loadingSpinner {
font-size: 32px;
margin-bottom: 16px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive Design */
@media (max-width: 768px) {
.header {
padding: 16px;
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.title {
margin-bottom: 20px;
font-weight: bold;
.content {
padding: 16px;
}
.uploadMessage {
font-weight: 600;
text-align: left;
.detailGrid {
grid-template-columns: 1fr;
gap: 12px;
}
.changeButtonEnabled {
padding-right: 10px;
background-color: green;
border-radius: 30px;
color: white;
font-weight: 700;
height: 36px;
line-height: 36px;
padding-left: 10px;
padding-height: 10px;
.formGrid {
grid-template-columns: 1fr;
gap: 16px;
}
.changeButtonDisabled {
padding-right: 10px;
background-color: #a1a1a1;
border-radius: 30px;
color: white;
font-weight: 700;
height: 36px;
line-height: 36px;
padding-left: 10px;
padding-height: 10px;
.historyItems {
max-height: 200px;
}
.resultMessage {
margin-top: -13px;
text-align: left;
display: flex;
justify-content: space-between;
.materialSummary {
padding: 14px 16px;
}
.resultMessage input {
padding-left: 8px;
width: 180px;
.materialName {
font-size: 15px;
}
.stokContainer {
display: flex;
justify-content: space-evenly;
align-items: center;
margin-top: -20px;
margin-bottom: -15px;
text-align: left;
.materialStock {
font-size: 13px;
margin-right: 12px;
}
.buttonContainer {
margin-top: 11px;
text-align: left;
}
.stockButton {
padding: 10px 20px;
font-size: 3.5vw;
background-color: #28a745;
color: #fff;
border: none;
border-radius: 30px;
cursor: pointer;
transition: background-color 0.3s;
}
.saveButton {
width: 100%;
padding: 10px 20px;
font-size: 3.5vw;
background-color: #28a745;
color: #fff;
border: none;
border-radius: 30px;
cursor: pointer;
transition: background-color 0.3s;
}
.switchContainer {
margin-top: 20px;
text-align: left;
}
.historyTab {
text-align: left;
}
.historyContainer {
text-align: left;
max-height: 15vh;
overflow-y: auto;
}
.description {
display: flex;
margin: 10px 0;
font-size: 14px;
color: #666;
}
.unit {
margin-top: 11px;
width: 100%;
height: 31px;
}
.sorter {
border: 1px solid #c3c3c3;
padding: 5px;
border-radius: 10px;
display: flex;
justify-content: space-between;
margin-top: -10px;
margin-bottom: 10px;
}
.mutationCard {
display: flex;
margin-bottom: 7px;
margin-top: 7px;
}
.mutationTitle {
margin-left: 5px;
}
.mutationTitle h4 {
margin: 0;
}
.mutationTitle p {
margin: 0;
.container {
max-height: 75vh; /* Menaikkan tinggi maksimum pada mobile */
}
}

View File

@@ -82,7 +82,7 @@ const RoundedRectangle = ({
};
const percentageStyle = {
fontSize: "16px",
fontSize: "14px",
display: "flex",
alignItems: "center",
textAlign: "right",
@@ -282,11 +282,11 @@ const App = ({ forCafe = true, cafeId = -1,
if (amount >= 1_000_000_000) {
// Format for billions
const billions = amount / 1_000_000_000;
return billions.toFixed(0) + "b"; // No decimal places for billions
return billions.toFixed(0) + "m"; // No decimal places for billions
} else if (amount >= 1_000_000) {
// Format for millions
const millions = amount / 1_000_000;
return millions.toFixed(2).replace(/\.00$/, "") + "m"; // Two decimal places, remove trailing '.00'
return millions.toFixed(2).replace(/\.00$/, "") + "jt"; // Two decimal places, remove trailing '.00'
} else if (amount >= 1_000) {
// Format for thousands
const thousands = amount / 1_000;
@@ -326,7 +326,7 @@ const App = ({ forCafe = true, cafeId = -1,
setSelectedCafeId(-1);
} else if (otherCafes.length === 1) {
updatedFullTexts = [
[otherCafes[0].cafeIdentifyName || otherCafes[0].username, otherCafes[0].cafeId || otherCafes[0].userId],
[otherCafes[0].cafeIdentifyName || otherCafes[0].username, otherCafes[0].cafeId || otherCafes[0].user_id],
// Only add the "Buat Bisnis" option for user.roleId == 1
...(user.roleId == 1 ? [["Buat Bisnis", -1]] : [])
];
@@ -335,7 +335,7 @@ const App = ({ forCafe = true, cafeId = -1,
} else {
updatedFullTexts = [
["semua", 0], // First entry is "semua"
...otherCafes.map(item => [item.cafeIdentifyName || item.username, item.cafeId || item.userId]), // Map over cafes to get name and cafeId pairs
...otherCafes.map(item => [item.cafeIdentifyName || item.username, item.cafeId || item.user_id]), // Map over cafes to get name and cafeId pairs
// Only add "Buat Bisnis +" option for user.roleId == 1
...(user.roleId == 1 ? [["Buat Bisnis +", -1]] : [])
];
@@ -411,10 +411,10 @@ const App = ({ forCafe = true, cafeId = -1,
console.log(analytics)
if (user && user.roleId === 0 && analytics) {
// Filter the analytics items based on userId
// Filter the analytics items based on user_id
if(selectedItem[1] != 0 && selectedItem[1] != -1){
const filteredData = analytics.items.filter(
(data) => data.userId === nextSelectedId
(data) => data.user_id === nextSelectedId
);
// Extract coupons from the filtered data

View File

@@ -0,0 +1,76 @@
/* StickyCartBar.module.css */
.bar {
position: sticky;
bottom: 40px;
z-index: 120; /* above items, below modal */
display: flex;
justify-content: center;
align-items: center;
margin-top: 10px;
pointer-events: none; /* allow buttons to define interaction */
}
.row {
display: flex;
gap: 8px;
width: 100%;
max-width: 980px;
padding: 0 12px;
pointer-events: auto;
}
.mainBtn {
flex: 1;
height: 44px;
border-radius: 999px;
background: var(--brand-primary, #73a585);
color: #fff;
border: none;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
cursor: pointer;
box-shadow: 0 6px 18px rgba(115,165,133,0.35);
}
.summary {
display: flex;
align-items: center;
gap: 6px;
font-weight: 600;
}
.value {
white-space: nowrap;
font-weight: 700;
}
.cartIcon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
}
.historyBtn {
width: 48px;
height: 44px;
border-radius: 999px;
background: var(--brand-primary, #73a585);
color: #fff;
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 6px 18px rgba(115,165,133,0.35);
}
@media (max-width: 480px) {
.mainBtn { height: 42px; }
.historyBtn { height: 42px; width: 46px; }
}

View File

@@ -11,7 +11,7 @@ import { getTables } from "../helpers/tableHelper";
import TableCanvas from "../components/TableCanvas";
import { useSearchParams } from "react-router-dom";
export default function Transactions({ propsShopId, sendParam, deviceType, handleMoveToTransaction, depth, shopImg }) {
export default function Transactions({ propsShopId, sendParam, deviceType, handleMoveToTransaction, depth, shopImg, setModal }) {
const { shopId, tableId } = useParams();
if (sendParam) sendParam({ shopId, tableId });
@@ -231,13 +231,29 @@ export default function Transactions({ propsShopId, sendParam, deviceType, handl
</div>
))}
</div>
{!transaction.is_paid && transaction.confirmed > -1 &&
<div
onClick={() => {
localStorage.setItem('lastTransaction', JSON.stringify(transaction));
setModal("message", { captMessage: 'Silahkan tambahkan pesanan', descMessage: 'Pembayaran akan ditambahkan ke transaksi sebelumnya.' }, null, null);
// Dispatch the custom event
window.dispatchEvent(new Event("localStorageUpdated"));
}}
className={styles["addNewItem"]}
>
Tambah pesanan
</div>
}
<h2 className={styles["Transactions-detail"]}>
{transaction.serving_type === "pickup"
? "Ambil sendiri"
: `Diantar ke ${transaction.Table ? transaction.Table.tableNo : "N/A"
}`}
</h2>
{transaction.notes != null && (
{transaction.notes != '' && (
<>
<div className={styles.NoteContainer}>
<span>Note :</span>

View File

@@ -192,7 +192,7 @@ export default function Transactions({
))}
</ul>
{transaction.payment_type != 'paylater/cash' && transaction.payment_type != 'paylater/cashless' &&
{(transaction.payment_type != 'paylater/cash' && transaction.payment_type != 'paylater/cashless') &&
<div
onClick={() => {
localStorage.setItem('lastTransaction', JSON.stringify(transaction));

View File

@@ -8,100 +8,121 @@ import {
declineTransaction,
getTransactionsFromCafe,
} from "../helpers/transactionHelpers";
import { getTables } from "../helpers/tableHelper";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import { ThreeDots } from "react-loader-spinner";
import ButtonWithReplica from "../components/ButtonWithReplica";
import TableCanvas from "../components/TableCanvas";
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
dayjs.extend(utc);
dayjs.extend(timezone);
export default function Transactions({ shop, shopId, propsShopId, sendParam, deviceType, paymentUrl }) {
export default function Transactions({ shop, shopId, propsShopId, sendParam, deviceType, paymentUrl, setModal, newTransaction }) {
const { shopIdentifier, tableId } = useParams();
if (sendParam) sendParam({ shopIdentifier, tableId });
dayjs.extend(utc);
dayjs.extend(timezone);
const [transactions, setTransactions] = useState([]);
const [isPaymentLoading, setIsPaymentLoading] = useState(false);
const [isPaymentOpen, setIsPaymentOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [matchedItems, setMatchedItems] = useState([]);
const [loading, setLoading] = useState(false);
// Currency formatter (thousand separators, with Rp prefix)
const formatRp = (value) => `Rp ${new Intl.NumberFormat('id-ID').format(Math.round(Number(value || 0)))}`;
const getStatusClass = (t) => {
if (t.confirmed === 3 || t.is_paid) return styles.statusSuccess;
if (t.confirmed === -1 || t.confirmed === -2) return styles.statusCancelled;
return styles.statusNeutral;
};
useEffect(() => {
setMatchedItems(searchAndAggregateItems(transactions, searchTerm));
}, [searchTerm, transactions]);
useEffect(() => {
const fetchTransactions = async () => {
if (deviceType == 'clerk') {
try {
let response;
response = await getTransactionsFromCafe(shopId || propsShopId, 5, false);
console.log(response)
if (response) {
setTransactions(response);
return;
}
} catch (error) {
console.error("Error fetching transactions:", error);
}
}
else {
try {
let response;
response = await getMyTransactions(shopId || propsShopId, 5);
console.log(response)
const combinedTransactions = [];
try {
response.forEach(cafe => {
const { cafeId, name: cafeName, transactions } = cafe;
// response = await getMyTransactions(shopId || propsShopId, 5);
// setMyTransactions(response);
setLoading(true);
let response = await getTransactionsFromCafe(shopId || propsShopId, -1, false);
transactions.forEach(transaction => {
const newTransaction = {
...transaction,
cafeId,
cafeName,
DetailedTransactions: transaction.detailedTransactions // Rename here
};
delete newTransaction.detailedTransactions; // Remove the old key
combinedTransactions.push(newTransaction);
});
});
// combinedTransactions now contains all transactions with cafe info and renamed key
console.log(combinedTransactions)
// combinedTransactions now contains all transactions with cafe info
setTransactions(combinedTransactions);
} catch (error) {
console.error("Error fetching transactions:", error);
}
setLoading(false);
if (response) setTransactions(response);
} catch (error) {
console.error("Error fetching transactions:", error);
}
};
console.log(deviceType)
fetchTransactions();
}, [deviceType]);
}, [deviceType, newTransaction]);
const calculateTotalPrice = (detailedTransactions) => {
return detailedTransactions.reduce((total, dt) => {
return total + dt.qty * (dt.promoPrice ? dt.promoPrice : dt.price);
}, 0);
};
const calculateAllTransactionsTotal = (transactions) => {
return transactions.reduce((grandTotal, transaction) => {
const calculateAllTransactionsTotal = (transactions) => {
return transactions
.filter(transaction => transaction.confirmed > 1) // Filter transactions where confirmed > 1
.reduce((grandTotal, transaction) => {
return grandTotal + calculateTotalPrice(transaction.DetailedTransactions);
}, 0);
};
};
const searchAndAggregateItems = (transactions, searchTerm) => {
if (!searchTerm.trim()) return [];
const normalizedTerm = searchTerm.trim().toLowerCase();
// Map with key = `${itemId}-${confirmedGroup}` to keep confirmed groups separate
const aggregatedItems = new Map();
transactions.forEach(transaction => {
// Determine confirmed group as a string key
const confirmedGroup = transaction.confirmed >= 0 && transaction.confirmed > 1 ? 'confirmed_gt_1' : 'confirmed_le_1';
transaction.DetailedTransactions.forEach(detail => {
const itemName = detail.Item.name;
const itemNameLower = itemName.toLowerCase();
if (itemNameLower.includes(normalizedTerm)) {
// Combine itemId and confirmedGroup to keep them separated
const key = `${detail.itemId}-${confirmedGroup}`;
if (!aggregatedItems.has(key)) {
aggregatedItems.set(key, {
itemId: detail.itemId,
name: itemName,
totalQty: 0,
totalPrice: 0,
confirmedGroup, // Keep track of which group this belongs to
});
}
const current = aggregatedItems.get(key);
current.totalQty += detail.qty;
current.totalPrice += detail.qty * (detail.promoPrice || detail.price);
}
});
});
console.log(aggregatedItems.values())
return Array.from(aggregatedItems.values());
};
const handleConfirm = async (transactionId) => {
setIsPaymentLoading(true);
try {
const c = await confirmTransaction(transactionId);
if (c) {
// Update the confirmed status locally
setTransactions((prevTransactions) =>
prevTransactions.map((transaction) =>
transaction.transactionId === transactionId
? { ...transaction, confirmed: 1 } // Set to confirmed
: transaction
const result = await confirmTransaction(transactionId);
if (result) {
setTransactions(prev =>
prev.map(t =>
t.transactionId === transactionId ? result : t
)
);
}
@@ -115,14 +136,11 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
const handleDecline = async (transactionId) => {
setIsPaymentLoading(true);
try {
const c = await declineTransaction(transactionId);
if (c) {
// Update the confirmed status locally
setTransactions((prevTransactions) =>
prevTransactions.map((transaction) =>
transaction.transactionId === transactionId
? c // Set to confirmed
: transaction
const result = await declineTransaction(transactionId);
if (result) {
setTransactions(prev =>
prev.map(t =>
t.transactionId === transactionId ? result : t
)
);
}
@@ -133,106 +151,109 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
}
};
if (loading)
return (
<div className="Loader">
<div className="LoaderChild">
<ThreeDots />
<h1></h1>
</div>
</div>
);
return (
<div className={styles.Transactions}>
<div style={{ marginTop: "30px" }}></div>
<h2 className={styles["Transactions-title"]}>Daftar transaksi
Rp {calculateAllTransactionsTotal(transactions)} </h2>
<div style={{ marginTop: "30px" }}></div>
{/* <TableCanvas tables={tables} selectedTable={selectedTable} /> */}
<div className={styles.TransactionListContainer} style={{ padding: '0 20px 0 20px' }}>
<h2 className={styles["Transactions-title"]}>
Transaksi selesai {formatRp(calculateAllTransactionsTotal(transactions))}
</h2>
<input
type="text"
placeholder="Cari nama item..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{ border: '0px', height: '36px', borderRadius: '12px', margin: '6px auto 10px', width: '88%', padding: '0 8px', fontSize: '14px' }}
/>
{/* Existing Transactions List (keep all your JSX below unchanged) */}
<div className={styles.TransactionListContainer}>
{matchedItems.length > 0 && matchedItems.map(item => (
<div
key={`${item.itemId}-${item.confirmedGroup}`}
className={styles.RoundedRectangle}
style={{ overflow: "hidden" }}
>
<ul>
<li>
<strong>{item.name}</strong> x {item.totalQty}
</li>
</ul>
<div className={styles.TotalContainer}>
<span>Total:</span>
<span>{formatRp(item.totalPrice)}</span>
</div>
</div>
))}
{transactions &&
transactions.map((transaction) => (
<div
key={transaction.transactionId}
className={styles.RoundedRectangle}
style={{ overflow: 'hidden' }}
className={`${styles.RoundedRectangle} ${!transaction.is_paid ? styles.unpaid : ''}`}
>
<div className={styles['receipt-header']}>
{transaction.confirmed === 1 ? (
<ColorRing className={styles['receipt-logo']} />
) : transaction.confirmed === -1 || transaction.confirmed === -2 ? (
<div style={{ display: 'flex', justifyContent: 'center', margin: '16px 0px' }}>
<svg
style={{ width: '60px', transform: 'Rotate(45deg)' }}
clipRule="evenodd"
fillRule="evenodd"
strokeLinejoin="round"
strokeMiterlimit="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12.002 2c5.518 0 9.998 4.48 9.998 9.998 0 5.517-4.48 9.997-9.998 9.997-5.517 0-9.997-4.48-9.997-9.997 0-5.518 4.48-9.998 9.997-9.998zm0 1.5c-4.69 0-8.497 3.808-8.497 8.498s3.807 8.497 8.497 8.497 8.498-3.807 8.498-8.497-3.808-8.498-8.498-8.498zm-.747 7.75h-3.5c-.414 0-.75.336-.75.75s.336.75.75.75h3.5v3.5c0 .414.336.75.75.75s.75-.336.75-.75v-3.5h3.5c.414 0 .75-.336.75-.75s-.336-.75-.75-.75h-3.5v-3.5c0-.414-.336-.75-.75-.75s-.75.336-.75.75z"
fillRule="nonzero"
/>
) : (transaction.confirmed === -1 && !transaction.is_paid) || (transaction.confirmed === -2 && !transaction.is_paid) ? (
<div style={{ display: 'flex', justifyContent: 'center', margin: '12px 0' }}>
<svg width="60" height="60" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M18.3 5.7a1 1 0 0 0-1.4 0L12 10.6 7.1 5.7A1 1 0 0 0 5.7 7.1L10.6 12l-4.9 4.9a1 1 0 1 0 1.4 1.4L12 13.4l4.9 4.9a1 1 0 0 0 1.4-1.4L13.4 12l4.9-4.9a1 1 0 0 0 0-1.4z" fill="#E45454"/>
</svg>
</div>
) : transaction.confirmed === 2 ? (
) : transaction.confirmed === 2 && !transaction.is_paid ? (
<ColorRing className={styles['receipt-logo']} />
) : transaction.confirmed === 3 ? (
<div>
<svg
height="60px"
width="60px"
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
viewBox="0 0 506.4 506.4"
xmlSpace="preserve"
fill="#000000"
style={{marginTop: '12px', marginBottom: '12px'}}
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<circle style={{ fill: '#54B265' }} cx="253.2" cy="253.2" r="249.2" />
<path
style={{ fill: '#F4EFEF' }}
d="M372.8,200.4l-11.2-11.2c-4.4-4.4-12-4.4-16.4,0L232,302.4l-69.6-69.6c-4.4-4.4-12-4.4-16.4,0 L134.4,244c-4.4,4.4-4.4,12,0,16.4l89.2,89.2c4.4,4.4,12,4.4,16.4,0l0,0l0,0l10.4-10.4l0.8-0.8l121.6-121.6 C377.2,212.4,377.2,205.2,372.8,200.4z"
></path>
<path d="M253.2,506.4C113.6,506.4,0,392.8,0,253.2S113.6,0,253.2,0s253.2,113.6,253.2,253.2S392.8,506.4,253.2,506.4z M253.2,8 C118,8,8,118,8,253.2s110,245.2,245.2,245.2s245.2-110,245.2-245.2S388.4,8,253.2,8z"></path>
<path d="M231.6,357.2c-4,0-8-1.6-11.2-4.4l-89.2-89.2c-6-6-6-16,0-22l11.6-11.6c6-6,16.4-6,22,0l66.8,66.8L342,186.4 c2.8-2.8,6.8-4.4,11.2-4.4c4,0,8,1.6,11.2,4.4l11.2,11.2l0,0c6,6,6,16,0,22L242.8,352.4C239.6,355.6,235.6,357.2,231.6,357.2z M154,233.6c-2,0-4,0.8-5.6,2.4l-11.6,11.6c-2.8,2.8-2.8,8,0,10.8l89.2,89.2c2.8,2.8,8,2.8,10.8,0l132.8-132.8c2.8-2.8,2.8-8,0-10.8 l-11.2-11.2c-2.8-2.8-8-2.8-10.8,0L234.4,306c-1.6,1.6-4,1.6-5.6,0l-69.6-69.6C158,234.4,156,233.6,154,233.6z"></path>
</g>
</svg>
</div>
) : transaction.confirmed === 3 || transaction.is_paid ? (
<div style={{ display: 'flex', justifyContent: 'center', margin: '12px 0' }}>
<svg width="60" height="60" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M9 16.2 4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4z" fill="#54B265"/>
</svg>
</div>
) : (
<ColorRing className={styles['receipt-logo']} />
)}
<div className={styles['receipt-info']}>
{deviceType == 'clerk' ?
<h3>{transaction.confirmed === 1 ? (
"Silahkan Cek Pembayaran"
) : transaction.confirmed === -1 ? (
"Dibatalkan Oleh Kasir"
) : transaction.confirmed === -2 ? (
"Dibatalkan Oleh Pelanggan"
) : transaction.confirmed === 2 ? (
"Sedang Diproses"
) : transaction.confirmed === 3 ? (
"Transaksi Sukses"
) : (
"Silahkan Cek Ketersediaan"
)}</h3>
:
<h3>{transaction.confirmed === 1 ? (
(transaction.payment_type == 'cash' ? 'Silahkan Bayar Ke Kasir' : "Silahkan Bayar Dengan QRIS")
) : transaction.confirmed === -1 ? (
"Dibatalkan Oleh Kasir"
) : transaction.confirmed === -2 ? (
"Dibatalkan Oleh Pelanggan"
) : transaction.confirmed === 2 ? (
"Sedang diproses"
) : transaction.confirmed === 3 ? (
"Transaksi Sukses"
) : (
"Memeriksa Ketersediaan "
)}</h3>}
{deviceType == 'clerk' ?
<h3 className={getStatusClass(transaction)}>{transaction.confirmed === 1 && !transaction.is_paid ? (
"Silahkan Cek Pembayaran"
) : transaction.confirmed === -1 && !transaction.is_paid ? (
"Dibatalkan Oleh Kasir"
) : transaction.confirmed === -2 && !transaction.is_paid ? (
"Dibatalkan Oleh Pelanggan"
) : transaction.confirmed === 2 && !transaction.is_paid ? (
"Sedang Diproses"
) : transaction.confirmed === 3 || transaction.is_paid ? (
"Transaksi Sukses"
) : (
"Silahkan Cek Ketersediaan"
)}</h3>
:
<h3 className={getStatusClass(transaction)}>{transaction.confirmed === 1 ? (
(transaction.payment_type == 'cash' ? 'Silahkan Bayar Ke Kasir' : "Silahkan Bayar Dengan QRIS")
) : transaction.confirmed === -1 ? (
"Dibatalkan Oleh Kasir"
) : transaction.confirmed === -2 ? (
"Dibatalkan Oleh Pelanggan"
) : transaction.confirmed === 2 ? (
"Sedang diproses"
) : transaction.confirmed === 3 ? (
"Transaksi Sukses"
) : (
"Memeriksa Ketersediaan "
)}</h3>}
<p>Transaction ID: {transaction.transactionId}</p>
<p>Payment Type: {transaction.payment_type}</p>
@@ -258,16 +279,28 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
<ul>
{transaction.DetailedTransactions.map((detail) => (
<li key={detail.detailedTransactionId}>
<span>{detail.Item.name}</span> - {detail.qty < 1 ? 'tidak tersedia' : `${detail.qty} x Rp
${detail.promoPrice ? detail.promoPrice : detail.price}`}
<span>{detail.Item.name}</span> - {detail.qty < 1 ? 'tidak tersedia' : `${detail.qty} x ${formatRp(detail.promoPrice ? detail.promoPrice : detail.price)}`}
</li>
))}
</ul>
{!transaction.is_paid && transaction.confirmed > -1 && (
<div
onClick={() => {
localStorage.setItem('lastTransaction', JSON.stringify(transaction));
setModal("message", { captMessage: 'Silahkan tambahkan pesanan', descMessage: 'Pembayaran akan ditambahkan ke transaksi sebelumnya.' }, null, null);
// Dispatch the custom event
window.dispatchEvent(new Event("localStorageUpdated"));
}}
className={styles["addNewItem"]}
>
Tambah pesanan
</div>
)}
<h2 className={styles["Transactions-detail"]}>
{transaction.serving_type === "pickup"
? "Self pickup"
: `Serve to ${transaction.Table ? transaction.Table.tableNo : "N/A"
}`}
: `Serve to ${transaction.Table ? transaction.Table.tableNo : "N/A"}`}
</h2>
{transaction.notes && (
@@ -290,38 +323,44 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
<div className={styles.TotalContainer}>
<span>Total:</span>
<span>
Rp {calculateTotalPrice(transaction.DetailedTransactions)}
{formatRp(calculateTotalPrice(transaction.DetailedTransactions))}
</span>
</div>
<div className={styles.TotalContainer}>
{(deviceType == 'clerk' && (transaction.confirmed == 0 || transaction.confirmed == 1 || transaction.confirmed == 2)) &&
<button
className={styles.PayButton}
onClick={() => handleConfirm(transaction.transactionId)}
disabled={isPaymentLoading}
>
{
isPaymentLoading ? (
<ColorRing height="50" width="50" color="white" />
) : transaction.confirmed === 1 ? (
"Konfirmasi Telah Bayar"
) : transaction.confirmed === 2 ? (
"Confirm item is ready"
) : (
"Confirm availability"
)
}
{(deviceType == 'clerk' && !transaction.is_paid && (transaction.confirmed == 0 || transaction.confirmed == 1 || transaction.confirmed == 2)) && (
<div className={styles.ActionRow}>
<button
className={styles.PayButton}
onClick={() => handleConfirm(transaction.transactionId)}
disabled={isPaymentLoading}
>
{
isPaymentLoading ? (
<ColorRing height="28" width="28" color="white" />
) : transaction.confirmed === 1 ? (
"Konfirmasi"
) : transaction.confirmed === 2 ? (
"Confirm item is ready"
) : (
"Confirm availability"
)
}
</button>
<button
className={styles.DeclineButton}
onClick={() => handleDecline(transaction.transactionId)}
disabled={isPaymentLoading}
>
{isPaymentLoading ? '...' : 'Batalkan'}
</button>
</div>
)}
</button>
}
{deviceType == 'guestDevice' && transaction.confirmed < 2 && transaction.payment_type != 'cash' && transaction.payment_type != 'paylater/cash' &&
{deviceType == 'guestDevice' && transaction.confirmed < 2 && transaction.payment_type != 'cash' && transaction.payment_type != 'paylater/cash' &&
<ButtonWithReplica
paymentUrl={paymentUrl}
price={
"Rp" + calculateTotalPrice(transaction.DetailedTransactions)
}
price={formatRp(calculateTotalPrice(transaction.DetailedTransactions))}
disabled={isPaymentLoading}
isPaymentLoading={isPaymentLoading}
handleClick={() => handleConfirm(transaction.transactionId)}
@@ -344,40 +383,30 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
}
</div>
{deviceType == 'guestDevice' && transaction.confirmed >=0 && transaction.confirmed < 2 && transaction.payment_type == 'cash' ?
<button
className={styles.PayButton}
onClick={() => handleDecline(transaction.transactionId)}
disabled={
transaction.confirmed === -1 ||
transaction.confirmed === 3 ||
isPaymentLoading
} // Disable button if confirmed (1) or declined (-1) or
>
{isPaymentLoading ? (
<ColorRing height="50" width="50" color="white" />
) : transaction.confirmed === -1 ? (
"Ditolak" // Display "Declined" if the transaction is declined (-1)
) : transaction.confirmed === -2 ? (
"Dibatalkan" // Display "Declined" if the transaction is declined (-1)
) : (
"Batalkan" // Display "Confirm availability" if the transaction is not confirmed (0)
)}
</button>
:
((transaction.confirmed >= 0 && transaction.confirmed < 2 && transaction.payment_type != 'paylater/cash' && transaction.payment_type != 'paylater/cashless' || isPaymentOpen) &&
<h5
className={`${styles.DeclineButton}`}
onClick={() =>
isPaymentOpen
? setIsPaymentOpen(false)
: handleDecline(transaction.transactionId)
{deviceType == 'guestDevice' && (
transaction.confirmed >= 0 && transaction.confirmed < 2 && transaction.payment_type == 'cash' ? (
<button
className={styles.DeclineButton}
onClick={() => handleDecline(transaction.transactionId)}
disabled={
transaction.confirmed === -1 ||
transaction.confirmed === 3 ||
isPaymentLoading
}
>
{isPaymentOpen ? "kembali" : "batalkan"}
</h5>
{isPaymentLoading ? '...' : 'Batalkan'}
</button>
) : (
(transaction.confirmed >= 0 && transaction.confirmed < 2 && (transaction.payment_type != 'paylater/cash' && transaction.payment_type != 'paylater/cashless' || isPaymentOpen)) && (
<button
className={styles.DeclineButton}
onClick={() => isPaymentOpen ? setIsPaymentOpen(false) : handleDecline(transaction.transactionId)}
>
{isPaymentOpen ? 'Kembali' : 'Batalkan'}
</button>
)
)
}
)}
</div>
))}
</div>

View File

@@ -13,41 +13,43 @@
}
.Transactions {
overflow-x: hidden;
background-color: white;
background-color: #f5f7f6;
display: flex;
flex-direction: column;
justify-content: flex-start;
font-size: calc(10px + 2vmin);
color: rgba(88, 55, 50, 1);
background-color: #e9e9e9;
color: rgba(40, 40, 40, 1);
}
.Transactions-title {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-weight: 700;
font-style: normal;
font-size: 6vw;
color: black;
font-size: clamp(18px, 3.6vw, 24px);
color: var(--brand-sage, #6B8F71);
text-align: left;
margin-left: 20px;
margin-top: 57px;
margin: 16px 16px 8px 16px;
}
.Transactions-detail {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-style: normal;
font-size: 15px;
color: rgba(88, 55, 50, 1);
font-size: 13px;
color: #555;
text-align: left;
margin-left: 20px;
margin-top: 17px;
margin-left: 16px;
margin-top: 12px;
}
.TransactionListContainer {
overflow-y: auto; /* Enables vertical scrolling */
background-color: #dbdbdb;
background-color: transparent;
overflow: visible;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 12px;
padding: 8px 12px 16px;
}
.TotalContainer {
@@ -57,14 +59,14 @@
/* width: 100%; */
margin: 0 auto;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;
font-weight: 700;
font-style: normal;
font-size: 15px;
font-size: 13px;
/* padding: 10px; */
box-sizing: border-box;
margin-bottom: 17px;
margin-left: 20px;
margin-right: 20px;
margin-bottom: 12px;
margin-left: 16px;
margin-right: 16px;
}
.PaymentContainer {
@@ -83,61 +85,66 @@
.PayButton {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-weight: 600;
font-style: normal;
font-size: 12px; /* Adjusted for better readability */
padding: 12px 16px; /* Added padding for a better look */
border-radius: 50px;
background-color: rgba(88, 55, 50, 1);
font-size: 12px;
padding: 10px 14px;
border-radius: 10px; /* square rounded */
background-color: var(--brand-primary, #73a585);
color: white;
border: none;
margin: 0 auto;
cursor: pointer;
display: block; /* Centering the button */
display: inline-flex;
align-items: center;
justify-content: center;
text-align: center;
}
.DeclineButton {
font-family: "Plus Jakarta Sans", sans-serif;
z-index: 201;
position: relative;
font-weight: 500;
font-weight: 600;
font-style: normal;
font-size: 15px;
padding: 12px 24px; /* Add some padding for spacing */
color: rgba(88, 55, 50, 1);
background-color: transparent; /* No background */
border: none; /* No border */
margin: 0 auto; /* Center horizontally */
font-size: 12px;
padding: 10px 14px;
color: #444;
background-color: #f0f0f0;
border: 1px solid #e0e0e0;
border-radius: 10px; /* square rounded */
cursor: pointer;
display: block; /* Center the text horizontally */
text-align: center; /* Center the text within the button */
display: inline-flex;
align-items: center;
justify-content: center;
}
.DeclineButton.active {
position: relative;
z-index: 201;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-style: normal;
font-size: 20px;
padding: 12px 24px; /* Add some padding for spacing */
color: rgba(88, 55, 50, 1);
background-color: transparent; /* No background */
border: none; /* No border */
margin: 0 auto; /* Center horizontally */
cursor: pointer;
display: block; /* Center the text horizontally */
text-align: center; /* Center the text within the button */
margin-bottom: 23px; /* Space at the bottom to match the PayButton */
.DeclineButton.active { opacity: 0.9; }
/* Row for primary/secondary action buttons */
.ActionRow {
display: flex;
gap: 8px;
width: 100%;
justify-content: center;
}
.RoundedRectangle {
position: relative;
border-radius: 20px;
padding: 15px; /* Adjusted for better spacing */
margin: 12px;
background-color: #f9f9f9;
border-radius: 16px;
padding: 12px 12px 10px;
margin: 0;
background-color: #ffffff;
border: 1px solid var(--brand-sage-100, #E9F3ED);
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.06);
display: flex;
flex-direction: column;
min-height: 280px; /* base height, grows with content */
}
/* Unpaid tickets: taller to keep content inside ticket design */
.unpaid {
min-height: 380px;
}
/* Self pickup accent: subtle brand accent */
/* removed pickup accent and badge per request */
.expression {
width: 100%;
}
@@ -201,20 +208,28 @@
}
.receipt-logo {
width: 80px;
height: 80px;
width: 60px;
height: 60px;
border-radius: 50%; /* Circular logo */
object-fit: cover;
margin-bottom: 10px;
margin-bottom: 8px;
}
.receipt-info h3 {
font-size: 16px;
margin: 5px 0;
font-size: 13px;
font-weight: 700;
color: var(--brand-sage, #6B8F71);
margin: 4px 0 2px;
}
/* Status-colors override */
.receipt-info h3.statusNeutral { color: var(--brand-sage, #6B8F71); }
.receipt-info h3.statusSuccess { color: #54B265; }
.receipt-info h3.statusCancelled { color: #E45454; }
.receipt-info p {
font-size: 14px;
font-size: 12px;
color: #666;
margin: 2px 0;
}
/* Dotted line with circular cutouts */
@@ -222,39 +237,61 @@
display: flex;
align-items: center;
justify-content: center;
margin: 15px 0;
margin: 12px 0 10px;
}
.dotted-line .line {
border-top: 13px dotted #dbdbdb;
border-top: 10px dotted #e9e9e9;
width: 100%;
margin: 0 18px;
}
.dotted-line .circle-left {
left: -25px;
left: -18px;
position: absolute;
width: 50px;
height: 50px;
width: 36px;
height: 36px;
border-radius: 50%;
background-color: #dbdbdb;
background-color: #e9e9e9;
display: flex; /* Use flexbox to center the inner circle */
justify-content: center; /* Center horizontally */
align-items: center; /* Center vertically */
}
.dotted-line .circle-right {
right: -25px;
right: -18px;
position: absolute;
width: 50px;
height: 50px;
width: 36px;
height: 36px;
border-radius: 50%;
background-color: #dbdbdb;
background-color: #e9e9e9;
display: flex; /* Use flexbox to center the inner circle */
justify-content: center; /* Center horizontally */
align-items: center; /* Center vertically */
}
/* No scroll body for items per request */
/* Compact list text */
.RoundedRectangle ul {
list-style: none;
padding: 0 12px 6px;
margin: 0;
}
.RoundedRectangle ul li {
font-size: 12px;
color: #333;
padding: 3px 0;
}
/* Grid density tweaks */
@media (min-width: 640px) {
.TransactionListContainer { gap: 14px; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
}
@media (min-width: 1024px) {
.TransactionListContainer { gap: 16px; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
}
.inner-circle {
width: 80%;
height: 80%;
@@ -389,10 +426,11 @@
.addNewItem{
width: 100%;
height: 27px;
background-color: rgb(115, 165, 133);
border-radius: 11px;
height: 28px;
background-color: var(--brand-primary, rgb(115, 165, 133));
border-radius: 10px; /* square rounded */
text-align: center;
color: white;
line-height: 27px;
line-height: 28px;
font-size: 12px; /* compact */
}

View File

@@ -7,6 +7,8 @@
width: 100%;
height: 100%;
text-align: center;
padding: 20px;
box-sizing: border-box;
}
.image-container {
@@ -15,6 +17,11 @@
overflow: hidden;
border-radius: 50%;
margin-bottom: 20px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
}
.fileInput {
@@ -24,38 +31,134 @@
.circular-image {
width: 100%;
height: 100%;
object-fit: contain;
object-fit: cover;
border-radius: 50%;
}
.welcoming-text {
font-size: 24px;
margin-bottom: 20px;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;
color: #333;
text-align: center;
max-width: 80%;
}
.get-started-button {
padding: 10px 20px;
padding: 12px 24px;
border: none;
border-radius: 25px;
background-color: #007bff; /* Bootstrap primary color */
background-color: #28a745; /* Bootstrap primary color */
color: white;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;
box-shadow: 0 2px 6px rgba(40, 167, 69, 0.2);
}
.get-started-button:hover {
background-color: #0056b3; /* Darker shade on hover */
background-color: #218838; /* Darker shade on hover */
box-shadow: 0 4px 10px rgba(40, 167, 69, 0.3);
}
.get-started-button:active {
transform: translateY(1px);
}
/* Fullscreen styles */
.fullscreen {
position: fixed;
top: 50%;
left: 50%;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
transform: translate(-50%, -50%);
z-index: 300;
z-index: 1000;
background-color: inherit;
}
.fullscreen .image-container {
width: 200px;
height: 200px;
}
.fullscreen .welcoming-text {
font-size: 32px;
margin-bottom: 30px;
}
.fullscreen .get-started-button {
padding: 16px 32px;
font-size: 20px;
}
/* Responsive design */
@media (max-width: 768px) {
.welcome-page {
padding: 15px;
}
.image-container {
width: 120px;
height: 120px;
}
.welcoming-text {
font-size: 20px;
margin-bottom: 16px;
}
.get-started-button {
padding: 10px 20px;
font-size: 14px;
}
.fullscreen .image-container {
width: 150px;
height: 150px;
}
.fullscreen .welcoming-text {
font-size: 24px;
margin-bottom: 20px;
}
.fullscreen .get-started-button {
padding: 12px 24px;
font-size: 16px;
}
}
@media (max-width: 480px) {
.image-container {
width: 100px;
height: 100px;
}
.welcoming-text {
font-size: 18px;
margin-bottom: 12px;
}
.get-started-button {
padding: 8px 16px;
font-size: 12px;
}
.fullscreen .image-container {
width: 120px;
height: 120px;
}
.fullscreen .welcoming-text {
font-size: 20px;
margin-bottom: 16px;
}
.fullscreen .get-started-button {
padding: 10px 20px;
font-size: 14px;
}
}

View File

@@ -1,5 +1,5 @@
// WelcomePage.js
import React,{useRef} from "react";
import React, { useRef } from "react";
import "./WelcomePage.css";
const WelcomePage = ({
@@ -15,6 +15,15 @@ const WelcomePage = ({
const handleImageClick = () => {
fileInputRef.current.click();
};
// SVG Icon for camera
const CameraIcon = () => (
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 19H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2z" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<circle cx="12" cy="13" r="4" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
return (
<div
className={`welcome-page ${isFullscreen ? "fullscreen" : ""}`} // Corrected the className syntax
@@ -29,10 +38,13 @@ const WelcomePage = ({
className="image-container"
>
{!isFullscreen &&
<div onClick={handleImageClick} style={{width: '100%', height: '100%', position:'absolute'}}>
<h1 style={{textAlign:'left'}}>
{image ? "Click To Change Image" : "Click To Add Image"}
</h1>
<div onClick={handleImageClick} style={{width: '100%', height: '100%', position:'absolute', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer'}}>
<div style={{textAlign: 'center', color: 'white'}}>
<CameraIcon />
<h1 style={{textAlign:'center', fontSize: '16px', margin: '10px 0 0 0'}}>
{image ? "Klik untuk mengganti gambar" : "Klik untuk menambahkan gambar"}
</h1>
</div>
<input
ref={fileInputRef} style={{display: 'none', width:'100%', height: '100%'}}type="file" accept="image/*" onChange={onImageChange} />
</div>
@@ -43,7 +55,7 @@ const WelcomePage = ({
{welcomingText}
</h1>
<button className="get-started-button" onClick={onGetStarted}>
Get Started
Mulai
</button>
</div>
);

View File

@@ -1,56 +1,395 @@
/* WelcomePageEditor.css */
.welcome-page-editor {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
background-color: rgb(207, 207, 207);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
width: 100%;
height: 100vh;
background-color: #ffffff;
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
overflow: hidden; /* contain scroll inside editor-content */
font-family: "Plus Jakarta Sans", sans-serif;
}
h2 {
margin-bottom: 20px;
.editor-header {
padding: 24px;
border-bottom: 1px solid #e6e6e6;
background-color: #f8f9fa;
}
.editor-title {
margin: 0;
font-weight: 700;
font-size: 24px;
color: #333;
}
input[type="file"] {
margin-bottom: 20px;
}
textarea {
.editor-content {
display: flex;
flex: 1; /* take remaining height */
width: 100%;
height: 100px; /* Adjust as needed */
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
resize: none;
margin-bottom: 20px;
font-size: 16px;
overflow-y: auto; /* enable vertical scroll in editor-content */
overflow-x: hidden;
justify-content: center;
align-items: flex-start;
padding: 16px;
box-sizing: border-box;
}
textarea:focus {
border-color: #007bff; /* Highlight border color on focus */
.config-panel {
width: 100%;
max-width: 960px;
padding: 24px;
border-right: none;
background-color: #fafafa;
overflow-y: visible;
display: flex;
flex-direction: column;
gap: 24px;
box-sizing: border-box;
}
.config-section {
background-color: #ffffff;
border-radius: 12px;
padding: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.section-title {
margin: 0 0 16px 0;
font-weight: 600;
font-size: 18px;
color: #333;
display: flex;
align-items: center;
gap: 10px;
}
.config-group {
margin-bottom: 20px;
}
.config-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
font-size: 14px;
color: #555;
}
.config-input {
width: 100%;
padding: 12px 16px;
border-radius: 10px;
border: 1px solid #ddd;
font-size: 15px;
color: #333;
transition: all 0.2s ease;
box-sizing: border-box;
}
.config-input:focus {
outline: none;
border-color: #28a745;
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.1);
}
label {
margin-bottom: 20px;
.color-input {
height: 45px;
padding: 8px;
cursor: pointer;
}
.textarea-input {
min-height: 80px;
resize: vertical;
font-family: "Plus Jakarta Sans", sans-serif;
}
.switch-container {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #eee;
}
.switch-label {
font-weight: 500;
font-size: 15px;
color: #333;
}
.save-button {
width: 100%;
padding: 14px 16px;
border-radius: 10px;
border: none;
background-color: #28a745;
color: white;
font-weight: 600;
font-size: 16px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 6px rgba(40, 167, 69, 0.2);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.save-button:hover {
background-color: #218838;
box-shadow: 0 4px 10px rgba(40, 167, 69, 0.3);
}
.save-button:disabled {
background-color: #a1a1a1;
cursor: not-allowed;
box-shadow: none;
}
.preview-button {
width: 100%;
padding: 12px 16px;
border-radius: 10px;
border: 1px solid #ddd;
background-color: #ffffff;
color: #333;
font-weight: 600;
font-size: 15px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.preview-button:hover {
background-color: #f2f2f2;
}
.loading-spinner {
width: 20px;
height: 20px;
border: 2px solid #ffffff;
border-top: 2px solid transparent;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.preview-panel {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.preview-header {
padding: 16px 24px;
border-bottom: 1px solid #e6e6e6;
background-color: #f8f9fa;
display: flex;
justify-content: space-between;
align-items: center;
}
.preview-title {
margin: 0;
font-weight: 600;
font-size: 18px;
color: #333;
}
.preview-container {
flex: 1;
padding: 24px;
overflow: auto;
display: flex;
justify-content: center;
align-items: center;
}
.welcome-preview {
width: 100%;
height: 100%;
border: 1px dashed #ccc; /* Preview border style */
border-radius: 8px;
padding: 20px;
max-width: 400px;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.fullscreen-toggle {
position: absolute;
bottom: 24px;
right: 24px;
width: 60px;
height: 60px;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.7);
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
z-index: 10;
}
.fullscreen-toggle:hover {
background-color: rgba(0, 0, 0, 0.9);
transform: scale(1.05);
}
.toggle-icon {
font-size: 24px;
transform: rotate(45deg);
}
/* Top actions row */
.top-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-bottom: 8px;
}
.top-actions .preview-button {
width: auto;
padding: 10px 14px;
}
/* Theme & color redesign */
.hex-input {
max-width: 140px;
}
.theme-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.color-row {
display: flex;
gap: 12px;
align-items: center;
}
.swatches {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.swatch {
width: 28px;
height: 28px;
border-radius: 6px;
border: 1px solid transparent;
cursor: pointer;
}
.contrast-card {
margin-top: 16px;
border-radius: 12px;
border: 1px solid #e6e6e6;
}
.contrast-inner {
padding: 16px;
}
.contrast-title {
font-size: 14px;
color: #666;
margin-bottom: 8px;
font-weight: 600;
}
.contrast-preview {
font-size: 18px;
font-weight: 600;
}
@media (max-width: 992px) {
.theme-grid {
grid-template-columns: 1fr;
}
}
.inline-preview {
margin-top: 12px;
background: #fff;
border: 1px solid #e6e6e6;
border-radius: 12px;
overflow: hidden;
}
.inline-preview-content {
padding: 16px;
min-height: 320px;
display: flex;
justify-content: center;
align-items: center;
background-color: #ffffff; /* Background for preview area */
box-sizing: border-box;
}
/* Responsive design */
@media (max-width: 992px) {
.editor-content {
flex-direction: column;
padding: 12px;
}
.config-panel {
width: 100%;
border-right: none;
border-bottom: none;
max-height: none;
}
.preview-panel {
height: 50vh;
}
}
@media (max-width: 768px) {
.editor-header {
padding: 16px;
}
.editor-title {
font-size: 20px;
}
.config-panel {
padding: 16px;
}
.config-section {
padding: 16px;
}
.section-title {
font-size: 16px;
}
.preview-container {
padding: 16px;
}
.inline-preview-content {
min-height: 260px;
padding: 12px;
}
}

View File

@@ -14,6 +14,7 @@ const WelcomePageEditor = ({ cafeId, welcomePageConfig }) => {
const [isWelcomePageActive, setIsWelcomePageActive] = useState(false); // State for the switch
const [loading, setLoading] = useState(false); // Loading state
const [isFullscreen, setIsFullscreen] = useState(false);
const [showPreview, setShowPreview] = useState(false);
// Load existing welcome page configuration when component mounts
useEffect(() => {
@@ -51,6 +52,21 @@ const WelcomePageEditor = ({ cafeId, welcomePageConfig }) => {
setTextColor(e.target.value);
};
// Extra handlers for HEX input fields
const handleBackgroundHexInput = (e) => {
let v = e.target.value;
if (!v.startsWith('#')) v = '#' + v.replace(/#/g, '');
if (v.length > 7) v = v.slice(0, 7);
setBackgroundColor(v);
};
const handleTextHexInput = (e) => {
let v = e.target.value;
if (!v.startsWith('#')) v = '#' + v.replace(/#/g, '');
if (v.length > 7) v = v.slice(0, 7);
setTextColor(v);
};
const handleSave = async () => {
setLoading(true);
@@ -72,85 +88,236 @@ const WelcomePageEditor = ({ cafeId, welcomePageConfig }) => {
}
};
// SVG Icons
const CoffeeIcon = () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 8h1.5a2.5 2.5 0 0 1 0 5H15m0-5H13m-1 0H6a3 3 0 0 0-3 3v1.5a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V11a3 3 0 0 0-3-3h-5.5m-1 0V5a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1V2m-5 0v1a1 1 0 0 0-1 1v1m0 0v1a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
const PaletteIcon = () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 3v4a1 1 0 0 0 1 1h4m-9 4a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm6 10v-2a3 3 0 0 0-3-3h-4a3 3 0 0 0-3 3v2h10ZM16 21v-2a3 3 0 0 0-3-3h-2a3 3 0 0 0-3 3v2h8Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
const TypeIcon = () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 7V4h16v3M9 20h6M12 4v16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
const SaveIcon = () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M17 21v-8H7v8M7 3v5h8M12 11h6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
const EyeIcon = () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8Z" stroke="currentColor" strokeWidth="2"/>
<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" stroke="currentColor" strokeWidth="2"/>
<path d="M22 12c-1.5-4-5-6-10-6S4 8 2 12" stroke="currentColor" strokeWidth="2"/>
</svg>
);
return (
<div
className="welcome-page-editor"
style={{ width: "80vw", height: "80vh" }}
>
<h2>Edit Welcome Page</h2>
<div style={{ display: "flex", flexDirection: "column" }}>
<textarea
value={welcomingText}
onChange={handleTextChange}
placeholder="Enter welcoming text..."
style={{ height: "20px", resize: "none" }} // Reduced height
/>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<label>
Background Color:
<input
type="color"
value={backgroundColor}
onChange={handleColorChange}
/>
</label>
<label>
Text Color:
<input
type="color"
value={textColor}
onChange={handleTextColorChange}
/>
</label>
</div>
<div style={{ display: "flex", alignItems: "center" }}>
<label style={{ marginRight: "10px" }}>Is Welcome Page Active:</label>
<Switch
onChange={() => setIsWelcomePageActive(!isWelcomePageActive)}
checked={isWelcomePageActive}
offColor="#888"
onColor="#0a0"
uncheckedIcon={false}
checkedIcon={false}
/>
</div>
<button onClick={handleSave} disabled={loading}>
{loading ? "Saving..." : "Save Configuration"}
</button>
<div className="welcome-page-editor">
<div className="editor-header">
<h2 className="editor-title">Konfigurasi Halaman Selamat Datang</h2>
</div>
<div
style={{ width: "100%", height: "100%", position: "relative", flex: 1, borderRadius: '15px' }}
>
<WelcomePage
image={image}
welcomingText={welcomingText}
backgroundColor={backgroundColor}
textColor={textColor}
onGetStarted={() => setIsFullscreen(false)}
isFullscreen={isFullscreen}
onImageChange={handleImageChange}
/>
<div style={{ position: "absolute", bottom: 0, right: 0 }}>
<svg
width="100" // Adjust size as needed
height="100" // Adjust size as needed
style={{ position: "absolute", bottom: 0, right: 0 }}
onClick={() => setIsFullscreen(true)}
<div className="editor-content">
{/* Configuration Panel */}
<div className="config-panel">
{/* Top Preview Toggle */}
<div className="top-actions">
<button
type="button"
className="preview-button"
onClick={() => setShowPreview(!showPreview)}
>
<EyeIcon /> {showPreview ? 'Tutup Pratinjau' : 'Preview'}
</button>
</div>
{showPreview && (
<div className="inline-preview">
<div className="inline-preview-content">
<WelcomePage
image={image}
welcomingText={welcomingText}
backgroundColor={backgroundColor}
textColor={textColor}
onGetStarted={() => setShowPreview(false)}
isFullscreen={false}
onImageChange={handleImageChange}
/>
</div>
</div>
)}
<div className="config-section">
<h3 className="section-title">
<CoffeeIcon />
Konten Utama
</h3>
<div className="config-group">
<label className="config-label">Teks Selamat Datang</label>
<textarea
className="config-input textarea-input"
value={welcomingText}
onChange={handleTextChange}
placeholder="Masukkan teks selamat datang..."
/>
</div>
<div className="config-group">
<label className="config-label">Gambar Latar Belakang</label>
<input
type="file"
accept="image/*"
onChange={handleImageChange}
className="config-input"
/>
{image && (
<div style={{ marginTop: '12px', textAlign: 'center' }}>
<img
src={image}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '150px',
borderRadius: '8px',
border: '1px solid #ddd'
}}
/>
</div>
)}
</div>
</div>
<div className="config-section">
<h3 className="section-title">
<PaletteIcon />
Tema & Warna
</h3>
<div className="theme-grid">
<div className="color-field">
<label className="config-label">Warna Latar Belakang</label>
<div className="color-row">
<input
type="color"
className="config-input color-input"
value={backgroundColor}
onChange={handleColorChange}
/>
<input
type="text"
className="config-input hex-input"
value={backgroundColor}
onChange={handleBackgroundHexInput}
maxLength={7}
placeholder="#FFFFFF"
/>
</div>
<div className="swatches" aria-label="Background presets">
{['#ffffff','#f8f9fa','#fff3cd','#e8f5e9','#e3f2fd','#212529'].map((c) => (
<button
key={c}
className="swatch"
style={{ backgroundColor: c, borderColor: c.toLowerCase()==='#ffffff'?'#ddd':'transparent' }}
onClick={() => setBackgroundColor(c)}
title={c}
type="button"
/>
))}
</div>
</div>
<div className="color-field">
<label className="config-label">Warna Teks</label>
<div className="color-row">
<input
type="color"
className="config-input color-input"
value={textColor}
onChange={handleTextColorChange}
/>
<input
type="text"
className="config-input hex-input"
value={textColor}
onChange={handleTextHexInput}
maxLength={7}
placeholder="#000000"
/>
</div>
<div className="swatches" aria-label="Text presets">
{['#000000','#212529','#343a40','#6c757d','#ffffff','#28a745'].map((c) => (
<button
key={c}
className="swatch"
style={{ backgroundColor: c, borderColor: c.toLowerCase()==='#ffffff'?'#ddd':'transparent' }}
onClick={() => setTextColor(c)}
title={c}
type="button"
/>
))}
</div>
</div>
</div>
<div className="contrast-card" style={{ backgroundColor }}>
<div className="contrast-inner">
<div className="contrast-title">Pratinjau Kontrast</div>
<div className="contrast-preview" style={{ color: textColor }}>{welcomingText || 'Contoh Teks'}</div>
</div>
</div>
</div>
<div className="config-section">
<h3 className="section-title">
<TypeIcon />
Pengaturan
</h3>
<div className="switch-container">
<span className="switch-label">Aktifkan Halaman Selamat Datang</span>
<Switch
onChange={() => setIsWelcomePageActive(!isWelcomePageActive)}
checked={isWelcomePageActive}
offColor="#cccccc"
onColor="#28a745"
uncheckedIcon={false}
checkedIcon={false}
height={24}
width={48}
handleDiameter={20}
/>
</div>
</div>
<button
className="save-button"
onClick={handleSave}
disabled={loading}
>
<g transform="rotate(45 50 50)">
<circle cx="50" cy="50" r="40" fill="rgba(0, 0, 0, 0.5)" />
<text
x="50"
y="50"
textAnchor="middle"
dominantBaseline="middle"
fontSize="24"
fill="white" // Adjust text color as needed
>
&lt;&gt;
</text>
</g>
</svg>
{loading ? (
<>
<div className="loading-spinner"></div>
Menyimpan...
</>
) : (
<>
<SaveIcon />
Simpan Konfigurasi
</>
)}
</button>
{/* Pratinjau dipicu oleh tombol atas; section khusus dihapus */}
</div>
</div>
</div>