diff --git a/src/app/(app)/my-orders/[id]/page.tsx b/src/app/(app)/my-orders/[id]/page.tsx index 2b8b538..b83c661 100644 --- a/src/app/(app)/my-orders/[id]/page.tsx +++ b/src/app/(app)/my-orders/[id]/page.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useState } from 'react' -import { useParams, useRouter } from 'next/navigation' +import { useParams, useRouter, notFound } from 'next/navigation' import { getOrderDetail, updateSubmissionPayment, updateOrderStatus, updateOrder, deleteOrder, getSessionUser, getBalancesAsCreator } from '@/app/actions' import { Card, CardContent } from '@/components/ui/card' import { Button, buttonVariants } from '@/components/ui/button' @@ -23,12 +23,14 @@ import { ToggleLeft, AlertCircle, Pencil, + Sparkles, ShoppingBag, PlusCircle, MinusCircle, Trash2, AlertTriangle } from 'lucide-react' +import { ShareButton } from '@/components/ShareButton' import Link from 'next/link' export default function OrderDetailPage() { @@ -104,17 +106,7 @@ export default function OrderDetailPage() { } if (!order) { - return ( -
-

Order tidak ditemukan.

- - Kembali ke Jasa Order - -
- ) + notFound() } const isCreator = userId === order.creator_id @@ -254,6 +246,11 @@ export default function OrderDetailPage() { {format(new Date(order.date), 'dd MMM yyyy')} + {order.allow_custom && ( +
+ Custom Item Aktif +
+ )}

{order.title} @@ -306,6 +303,14 @@ export default function OrderDetailPage() { )} + {order.status === 'OPEN' && ( + + )} + {/* Status Change Buttons */} {order.status === 'DRAFT' && isToday(new Date(order.date)) && ( + + {order.status === 'OPEN' && ( + + )} diff --git a/src/app/(app)/order/[id]/PublicOrderActions.tsx b/src/app/(app)/order/[id]/PublicOrderActions.tsx new file mode 100644 index 0000000..9c40b5b --- /dev/null +++ b/src/app/(app)/order/[id]/PublicOrderActions.tsx @@ -0,0 +1,35 @@ +'use client' + +import { useState } from 'react' +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { Dialog, DialogTrigger } from '@/components/ui/dialog' +import { ShoppingBag } from 'lucide-react' +import { OrderFormModal } from '@/components/OrderFormModal' +import { ShareButton } from '@/components/ShareButton' + +export function PublicOrderActions({ order }: { order: any }) { + const [open, setOpen] = useState(false) + + if (order.status !== 'OPEN') { + return null + } + + return ( +
+ + + + Titip Sekarang + + setOpen(false)} /> + + + +
+ ) +} diff --git a/src/app/(app)/order/[id]/page.tsx b/src/app/(app)/order/[id]/page.tsx new file mode 100644 index 0000000..e0a285b --- /dev/null +++ b/src/app/(app)/order/[id]/page.tsx @@ -0,0 +1,151 @@ +import { getOrderDetail, getSessionUser } from '@/app/actions' +import { redirect, notFound } from 'next/navigation' +import { Store, User, Users, CheckCircle2, ChevronLeft, MapPin, Sparkles } from 'lucide-react' +import Link from 'next/link' +import { PublicOrderActions } from './PublicOrderActions' +import { format } from 'date-fns' +import { id as idLocale } from 'date-fns/locale' +import { cn } from '@/lib/utils' + +export const dynamic = 'force-dynamic' + +export default async function PublicOrderDetailPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params + const session = await getSessionUser() + + if (!session) { + redirect(`/login?callbackUrl=/order/${id}`) + } + + const order = await getOrderDetail(id) + + if (!order) { + notFound() + } + + return ( +
+
+ + + +

Detail PO

+
+ + {/* Hero Card */} +
+
+ +
+
+ {order.creator.photo ? ( + {order.creator.name} + ) : ( + + )} +
+ +

+ {order.title} +

+ +
+ + + {order.creator.name} + + + + {order.status} + + {order.allow_custom && ( + + + Custom Item + + )} +
+ + +
+ +
+
+

Tanggal PO

+

+ {format(new Date(order.date), 'dd MMM yyyy', { locale: idLocale })} +

+
+
+

Total Orang

+

+ {order.submissions.length} menitip +

+
+
+

Menu Tersedia

+

+ {order.available_items.length} item +

+
+
+
+ + {/* Daftar Penitip (Submissions) */} +
+
+
+
+ +
+

Daftar Penitip

+
+
+ +
+ {order.submissions.length > 0 ? ( + order.submissions.map((sub: any) => ( +
+ {sub.user.photo ? ( + {sub.user.name} + ) : ( +
+ {sub.user.name.charAt(0).toUpperCase()} +
+ )} + +
+

{sub.user.name}

+
+ {sub.items.map((item: any) => ( + + {item.qty}x {item.name} + + ))} +
+
+
+ )) + ) : ( +
+

Belum ada yang menitip.

+

Jadilah yang pertama untuk menitip pesanan!

+
+ )} +
+
+
+ ) +} diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index 1d0e540..a6ebbbd 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -31,7 +31,9 @@ export default function LoginPage() { const res = await loginUser(username, password) if (res.success) { - window.location.href = '/' // Force hard reload to update context/middleware + const params = new URLSearchParams(window.location.search) + const callbackUrl = params.get('callbackUrl') || '/' + window.location.href = callbackUrl // Force hard reload to update context/middleware } else { setError(res.error || 'Gagal login') setLoading(false) @@ -142,9 +144,19 @@ export default function LoginPage() {

Belum punya akun?{' '} - + +

+ +

+ © {new Date().getFullYear()} TitipIn - Sistem Titip Pesanan by firmanramdhani

diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx index 9d2e3a4..c23305c 100644 --- a/src/app/(auth)/register/page.tsx +++ b/src/app/(auth)/register/page.tsx @@ -46,7 +46,9 @@ export default function RegisterPage() { const res = await registerUser(name, username, password) if (res.success) { - window.location.href = '/' // Force hard reload to update context/middleware + const params = new URLSearchParams(window.location.search) + const callbackUrl = params.get('callbackUrl') || '/' + window.location.href = callbackUrl // Force hard reload to update context/middleware } else { setError(res.error || 'Gagal mendaftar') setLoading(false) @@ -190,9 +192,19 @@ export default function RegisterPage() {

Sudah punya akun?{' '} - + +

+ +

+ © {new Date().getFullYear()} TitipIn - Sistem Titip Pesanan by firmanramdhani

diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 0000000..6cfaf79 --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,51 @@ +import Link from 'next/link' +import { Button } from '@/components/ui/button' +import { SearchX, ArrowLeft, Home } from 'lucide-react' + +export default function NotFound() { + return ( +
+ {/* Background ambient effects */} +
+
+
+
+ +
+
+ +
+ +
+ +

+ 404 +

+ +

+ Halaman Tidak Ditemukan +

+ +

+ Waduh, sepertinya halaman yang Anda cari sedang jalan-jalan atau memang tidak pernah ada. Mari kita kembali ke jalan yang benar! +

+ +
+ + + +
+ +
+ + {/* Footer info */} +

+ TitipIn © {new Date().getFullYear()} - Sistem Titip Pesanan by firmanramdhani +

+
+
+ ) +} diff --git a/src/components/OrderCard.tsx b/src/components/OrderCard.tsx index e0d9adc..ea8ce81 100644 --- a/src/components/OrderCard.tsx +++ b/src/components/OrderCard.tsx @@ -9,6 +9,8 @@ import { Label } from '@/components/ui/label' import { Input } from '@/components/ui/input' import { submitOrder, getUserSubmission, getSessionUser } from '@/app/actions' import { PlusCircle, MinusCircle, User, CheckCircle2, ShoppingBag, Sparkles, Users } from 'lucide-react' +import { OrderFormModal } from '@/components/OrderFormModal' +import { ShareButton } from '@/components/ShareButton' import { format } from 'date-fns' import { id as idLocale } from 'date-fns/locale' @@ -89,285 +91,26 @@ export function OrderCard({ order }: { order: any }) { {/* Right: Actions */} -
+
- - Titip Sekarang + + Titip Sekarang setOpen(false)} /> + +
) } -function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: () => void }) { - const [userId, setUserId] = useState('') - const [items, setItems] = useState>({}) - const [customItems, setCustomItems] = useState>([]) - const [loading, setLoading] = useState(false) - const [error, setError] = useState('') - useEffect(() => { - const init = async () => { - const user = await getSessionUser() - if (user?.id) { - setUserId(user.id) - loadExistingSubmission(user.id) - } - } - init() - }, [order.id]) - - const loadExistingSubmission = async (uid: string) => { - const sub = await getUserSubmission(order.id, uid) - if (sub) { - const newItems = { ...items } - const newCustoms: any[] = [] - - sub.items.forEach((item: any) => { - if (!item.is_custom) { - const stdItem = order.available_items.find((ai: any) => ai.name === item.name) - if (stdItem) { - newItems[stdItem.id] = { selected: true, qty: item.qty } - } - } else { - newCustoms.push({ id: Math.random().toString(), name: item.name, qty: item.qty }) - } - }) - setItems(newItems) - setCustomItems(newCustoms) - } - } - - const handleStandardItemToggle = (itemId: string, checked: boolean) => { - setItems(prev => ({ - ...prev, - [itemId]: { selected: checked, qty: checked ? 1 : 0 } - })) - } - - const handleStandardItemQty = (itemId: string, qty: number) => { - if (qty < 1) { - handleStandardItemToggle(itemId, false) - return - } - setItems(prev => ({ - ...prev, - [itemId]: { selected: true, qty } - })) - } - - const addCustomItem = () => { - setCustomItems([...customItems, { id: Math.random().toString(), name: '', qty: 1 }]) - } - - const updateCustomItem = (id: string, field: 'name' | 'qty', value: any) => { - setCustomItems(customItems.map(c => c.id === id ? { ...c, [field]: value } : c)) - } - - const removeCustomItem = (id: string) => { - setCustomItems(customItems.filter(c => c.id !== id)) - } - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setError('') - setLoading(true) - - const payloadItems: any[] = [] - - order.available_items.forEach((ai: any) => { - const state = items[ai.id] - if (state?.selected && state.qty > 0) { - payloadItems.push({ name: ai.name, qty: state.qty, is_custom: false }) - } - }) - - customItems.forEach(ci => { - if (ci.name.trim() && ci.qty > 0) { - payloadItems.push({ name: ci.name.trim(), qty: ci.qty, is_custom: true }) - } - }) - - if (payloadItems.length === 0) { - setError('Harap pilih minimal 1 item atau tambahkan item lainnya.') - setLoading(false) - return - } - - const res = await submitOrder({ - order_id: order.id, - user_id: userId, - items: payloadItems - }) - - if (res.success) { - onSuccess() - } else { - setError(res.error || 'Gagal menyimpan pesanan.') - } - setLoading(false) - } - - return ( - -
- Form Titipan - - {order.title} - -

- Pilih menu yang tersedia di bawah atau tambahkan item khusus jika diizinkan. -

-
- -
- {/* Standard Items */} -
- -
- {order.available_items.map((item: any) => { - const isSelected = items[item.id]?.selected || false - const qty = items[item.id]?.qty || 0 - return ( -
-
- handleStandardItemToggle(item.id, c as boolean)} - className="rounded-lg data-[state=checked]:bg-[#1B2CC1] data-[state=checked]:border-[#1B2CC1]" - /> - -
- {isSelected && ( -
- - {qty} - -
- )} -
- ) - })} - {order.available_items.length === 0 && ( -

- Tidak ada menu standar yang ditentukan. -

- )} -
-
- - {/* Custom Items */} - {order.allow_custom && ( -
-
- - -
- - {customItems.length > 0 ? ( -
- {customItems.map((ci, idx) => ( -
- {idx + 1}. - updateCustomItem(ci.id, 'name', e.target.value)} - className="flex-1 h-9 rounded-lg bg-white dark:bg-slate-900 text-xs font-medium" - /> - updateCustomItem(ci.id, 'qty', parseInt(e.target.value) || 1)} - className="w-16 h-9 rounded-lg bg-white dark:bg-slate-900 text-center font-bold text-xs" - /> - -
- ))} -
- ) : ( -
- -

Klik untuk Tambah Item Custom

-

Ingin titip menu lain? Masukkan nama dan kuantitasnya di sini.

-
- )} -
- )} - - {error && ( -
- {error} -
- )} - -
- -
-
-
- ) -} diff --git a/src/components/OrderFormModal.tsx b/src/components/OrderFormModal.tsx new file mode 100644 index 0000000..111a712 --- /dev/null +++ b/src/components/OrderFormModal.tsx @@ -0,0 +1,278 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Button } from '@/components/ui/button' +import { cn } from '@/lib/utils' +import { DialogContent, DialogTitle } from '@/components/ui/dialog' +import { Checkbox } from '@/components/ui/checkbox' +import { Label } from '@/components/ui/label' +import { Input } from '@/components/ui/input' +import { submitOrder, getUserSubmission, getSessionUser } from '@/app/actions' +import { PlusCircle, MinusCircle, CheckCircle2 } from 'lucide-react' + +export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: () => void }) { + const [userId, setUserId] = useState('') + const [items, setItems] = useState>({}) + const [customItems, setCustomItems] = useState>([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + + useEffect(() => { + const init = async () => { + const user = await getSessionUser() + if (user?.id) { + setUserId(user.id) + loadExistingSubmission(user.id) + } + } + init() + }, [order.id]) + + const loadExistingSubmission = async (uid: string) => { + const sub = await getUserSubmission(order.id, uid) + if (sub) { + const newItems: any = { ...items } + const newCustoms: any[] = [] + + sub.items.forEach((item: any) => { + if (!item.is_custom) { + const stdItem = order.available_items.find((ai: any) => ai.name === item.name) + if (stdItem) { + newItems[stdItem.id] = { selected: true, qty: item.qty } + } + } else { + newCustoms.push({ id: Math.random().toString(), name: item.name, qty: item.qty }) + } + }) + setItems(newItems) + setCustomItems(newCustoms) + } + } + + const handleStandardItemToggle = (itemId: string, checked: boolean) => { + setItems(prev => ({ + ...prev, + [itemId]: { selected: checked, qty: checked ? 1 : 0 } + })) + } + + const handleStandardItemQty = (itemId: string, qty: number) => { + if (qty < 1) { + handleStandardItemToggle(itemId, false) + return + } + setItems(prev => ({ + ...prev, + [itemId]: { selected: true, qty } + })) + } + + const addCustomItem = () => { + setCustomItems([...customItems, { id: Math.random().toString(), name: '', qty: 1 }]) + } + + const updateCustomItem = (id: string, field: 'name' | 'qty', value: any) => { + setCustomItems(customItems.map(c => c.id === id ? { ...c, [field]: value } : c)) + } + + const removeCustomItem = (id: string) => { + setCustomItems(customItems.filter(c => c.id !== id)) + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError('') + setLoading(true) + + const payloadItems: any[] = [] + + order.available_items.forEach((ai: any) => { + const state = items[ai.id] + if (state?.selected && state.qty > 0) { + payloadItems.push({ name: ai.name, qty: state.qty, is_custom: false }) + } + }) + + customItems.forEach(ci => { + if (ci.name.trim() && ci.qty > 0) { + payloadItems.push({ name: ci.name.trim(), qty: ci.qty, is_custom: true }) + } + }) + + if (payloadItems.length === 0) { + setError('Harap pilih minimal 1 item atau tambahkan item lainnya.') + setLoading(false) + return + } + + const res = await submitOrder({ + order_id: order.id, + user_id: userId, + items: payloadItems + }) + + if (res.success) { + onSuccess() + } else { + setError(res.error || 'Gagal menyimpan pesanan.') + } + setLoading(false) + } + + return ( + +
+ Form Titipan + + {order.title} + +

+ Pilih menu yang tersedia di bawah atau tambahkan item khusus jika diizinkan. +

+
+ +
+ {/* Standard Items */} +
+ +
+ {order.available_items.map((item: any) => { + const isSelected = items[item.id]?.selected || false + const qty = items[item.id]?.qty || 0 + return ( +
+
+ handleStandardItemToggle(item.id, c as boolean)} + className="rounded-lg data-[state=checked]:bg-[#1B2CC1] data-[state=checked]:border-[#1B2CC1]" + /> + +
+ {isSelected && ( +
+ + {qty} + +
+ )} +
+ ) + })} + {order.available_items.length === 0 && ( +

+ Tidak ada menu standar yang ditentukan. +

+ )} +
+
+ + {/* Custom Items */} + {order.allow_custom && ( +
+
+ + +
+ + {customItems.length > 0 ? ( +
+ {customItems.map((ci, idx) => ( +
+ {idx + 1}. + updateCustomItem(ci.id, 'name', e.target.value)} + className="flex-1 h-9 rounded-lg bg-white dark:bg-slate-900 text-xs font-medium" + /> + updateCustomItem(ci.id, 'qty', parseInt(e.target.value) || 1)} + className="w-16 h-9 rounded-lg bg-white dark:bg-slate-900 text-center font-bold text-xs" + /> + +
+ ))} +
+ ) : ( +
+ +

Klik untuk Tambah Item Custom

+

Ingin titip menu lain? Masukkan nama dan kuantitasnya di sini.

+
+ )} +
+ )} + + {error && ( +
+ {error} +
+ )} + +
+ +
+
+
+ ) +} diff --git a/src/components/ShareButton.tsx b/src/components/ShareButton.tsx new file mode 100644 index 0000000..94454bf --- /dev/null +++ b/src/components/ShareButton.tsx @@ -0,0 +1,53 @@ +'use client' + +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Share2, Check } from 'lucide-react' +import { cn } from '@/lib/utils' + +interface ShareButtonProps { + orderId: string + className?: string + variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" + size?: "default" | "sm" | "lg" | "icon" + showText?: boolean +} + +export function ShareButton({ orderId, className, variant = "outline", size = "sm", showText = true }: ShareButtonProps) { + const [copied, setCopied] = useState(false) + + const handleShare = () => { + const url = `${window.location.origin}/order/${orderId}` + navigator.clipboard.writeText(url) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + return ( + + ) +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 7618797..e6920b4 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -226,7 +226,7 @@ export function Sidebar({ isOpen, onClose }: { isOpen?: boolean; onClose?: () => {/* Bottom Feature Card */} -
+ {/*
@@ -236,6 +236,12 @@ export function Sidebar({ isOpen, onClose }: { isOpen?: boolean; onClose?: () =>

Gunakan Generator Rekap pada detail PO untuk salin ringkasan belanja otomatis ke chat grup.

+
*/} + {/* Copyright */} +
+

+ © {new Date().getFullYear()} TitipIn - Sistem Titip Pesanan
by firmanramdhani +