validation
User = get_user_model()
class LoginSerializer(serializers.Serializer):
email = serializers.EmailField()
password = serializers.CharField(write_only=True)
def validate(self, data):
email = data.get('email')
password = data.get('password')
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
raise serializers.ValidationError("Email tidak ditemukan")
if not user.check_password(password):
raise serializers.ValidationError("Password salah")
refresh = RefreshToken.for_user(user)
return {
'refresh': str(refresh),
'access': str(refresh.access_token),
'user': UserSerializer(user).data
}
register=
@api_view(['POST'])
def register(request):
try:
username = request.data.get('username')
password = request.data.get('password')
email = request.data.get('email')
no_hp = request.data.get('no_hp')
alamat = request.data.get('alamat')
if User.objects.filter(email=email).exists():
return Response({'error': 'Email sudah terdaftar'}, status=400)
if User.objects.filter(username=username).exists():
return Response({'error': 'Username sudah terdaftar'}, status=400)
user = User.objects.create_user(
username=username,
email=email,
no_hp=no_hp,
alamat=alamat
)
user.set_password(password)
user.save()
refresh = RefreshToken.for_user(user)
return Response({
'refresh': str(refresh),
'access': str(refresh.access_token),
'user': {
'id': user.id,
'username': user.username,
'email': user.email,
'status': user.status
}
})
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
"use client";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { zodResolver } from "@hookform/resolvers/zod";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { toast } from "sonner"; // Import Sonner
const formSchema = z.object({
username: z.string(),
password: z.string().min(5, "Password must be at least 5 characters long"),
});
const Login02Page = () => {
const [errorMessage, setErrorMessage] = useState("");
const router = useRouter();
const form = useForm<z.infer<typeof formSchema>>({
defaultValues: {
username: "",
password: "",
},
resolver: zodResolver(formSchema),
});
const onSubmit = async (data: z.infer<typeof formSchema>) => {
setErrorMessage("");
try {
const response = await fetch("http://127.0.0.1:8000/api/login/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
const result = await response.json();
if (response.ok) {
localStorage.setItem("adminToken", result.access);
toast.success("Login successful!"); // Success toast
router.push("/menu");
} else {
// Handle different error cases
if (result.detail) {
toast.error(result.detail); // Error toast from API
} else {
toast.error("Failed to login. Please check your credentials."); // Generic error
}
}
} catch (err: any) {
toast.error(err.message || "An unexpected error occurred"); // Network error
}
};
return (
<div className="min-h-screen flex items-center justify-center">
<div className="max-w-sm w-full flex flex-col items-center border rounded-lg p-6 shadow-sm">
<p className="mt-4 mb-10 text-xl font-bold tracking-tight">
Log in to Restoran SMK
</p>
<Form {...form}>
<form
className="w-full space-y-4"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input
type="text"
placeholder="Username"
className="w-full"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Password"
className="w-full"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="mt-4 w-full">
Login
</Button>
</form>
</Form>
<div className="mt-5 space-y-5">
<Link
href="#"
className="text-sm block underline text-muted-foreground text-center"
>
Forgot your password?
</Link>
<p className="text-sm text-center">
Don't have an account?
<Link href="#" className="ml-1 underline text-muted-foreground">
Create account
</Link>
</p>
</div>
</div>
</div>
);
};
export default Login02Page;
layout
import { Toaster } from "sonner";
// Di dalam komponen Layout Anda:
return (
<html>
<body>
{/* ... konten lainnya ... */}
<Toaster richColors position="top-center" />
</body>
</html>
);
# views.py
from django.db.models import Sum, Count, Case, When, Value, CharField
from rest_framework.decorators import api_view
from rest_framework.response import Response
from django.utils.dateparse import parse_date
from .models import DetailTransaksi, Transaksi
from datetime import datetime
@api_view(['GET'])
def top_5_menu_terlaris(request):
try:
# Tambahkan filter tanggal jika diperlukan
start_date = request.GET.get('start_date')
end_date = request.GET.get('end_date')
queryset = DetailTransaksi.objects.all()
if start_date and end_date:
start_date = datetime.strptime(start_date, '%Y-%m-%d').date()
end_date = datetime.strptime(end_date, '%Y-%m-%d').date()
queryset = queryset.filter(
transaksi__created_at__date__range=[start_date, end_date]
)
queryset = (
queryset
.values('menu__nama_menu')
.annotate(total_dipesan=Sum('qty'))
.order_by('-total_dipesan')[:5]
)
return Response({
'success': True,
'data': queryset,
'message': 'Data top 5 menu berhasil diambil'
})
except Exception as e:
return Response({
'success': False,
'error': str(e)
}, status=400)
@api_view(['GET'])
def total_pendapatan(request):
try:
start_date = request.GET.get('start_date', None)
end_date = request.GET.get('end_date', None)
if not start_date or not end_date:
return Response({
'success': False,
'error': 'Parameter start_date dan end_date diperlukan'
}, status=400)
start_date = datetime.strptime(start_date, '%Y-%m-%d').date()
end_date = datetime.strptime(end_date, '%Y-%m-%d').date()
# Hitung total pendapatan dari transaksi lunas
total_lunas = (
Transaksi.objects
.filter(kekurangan__lte=0, created_at__date__range=[start_date, end_date])
.aggregate(total_pendapatan=Sum('total_bayar'))['total_pendapatan'] or 0
)
# Hitung laba dari transaksi yang kurang (jika ada)
laba_transaksi_kurang = (
Transaksi.objects
.filter(kekurangan__gt=0, created_at__date__range=[start_date, end_date])
.aggregate(total_laba=Sum('total_bayar') - Sum('kekurangan'))['total_laba'] or 0
)
total_pendapatan = total_lunas + laba_transaksi_kurang
return Response({
'success': True,
'data': {
'total_pendapatan': total_pendapatan,
'detail': {
'pendapatan_lunas': total_lunas,
'laba_transaksi_kurang': laba_transaksi_kurang
}
}
})
except Exception as e:
return Response({
'success': False,
'error': str(e)
}, status=400)
@api_view(['GET'])
def ringkasan_status_pembayaran(request):
try:
start_date = request.GET.get('start_date', None)
end_date = request.GET.get('end_date', None)
if not start_date or not end_date:
return Response({
'success': False,
'error': 'Parameter start_date dan end_date diperlukan'
}, status=400)
start_date = datetime.strptime(start_date, '%Y-%m-%d').date()
end_date = datetime.strptime(end_date, '%Y-%m-%d').date()
queryset = (
Transaksi.objects
.filter(created_at__date__range=[start_date, end_date])
.annotate(
payment_status=Case(
When(status_pembayaran__in=['settlement', 'paid'], then=Value('Paid')),
When(status_pembayaran__in=['pending', 'unpaid'], then=Value('Unpaid')),
default=Value('Other'),
output_field=CharField(),
)
)
.values('payment_status')
.annotate(jumlah_transaksi=Count('id'))
.order_by('payment_status')
)
return Response({
'success': True,
'data': queryset,
'periode': f"{start_date} hingga {end_date}"
})
except Exception as e:
return Response({
'success': False,
'error': str(e)
}, status=400)
reporting
npm install @tanstack/react-table date-fns
/app/reports/page.tsx
/app/api/reports/route.ts
typescript
Copy
import { NextResponse } from 'next/server';
const API_BASE_URL = 'http://localhost:8000'; // Sesuaikan dengan URL Django Anda
export async function GET() {
const startDate = '2025-04-10';
const endDate = '2025-04-30';
try {
// Fetch data paralel dari semua endpoint
const [topMenuRes, pendapatanRes, statusRes] = await Promise.all([
fetch(`${API_BASE_URL}/report/top-menu/?start_date=${startDate}&end_date=${endDate}`),
fetch(`${API_BASE_URL}/report/pendapatan/?start_date=${startDate}&end_date=${endDate}`),
fetch(`${API_BASE_URL}/report/status-pembayaran/?start_date=${startDate}&end_date=${endDate}`)
]);
if (!topMenuRes.ok || !pendapatanRes.ok || !statusRes.ok) {
throw new Error('Gagal mengambil data dari server');
}
const [topMenu, pendapatan, statusPembayaran] = await Promise.all([
topMenuRes.json(),
pendapatanRes.json(),
statusRes.json()
]);
return NextResponse.json({
success: true,
data: {
topMenu: topMenu.data,
pendapatan: pendapatan.data,
statusPembayaran: statusPembayaran.data
},
periode: {
startDate,
endDate
}
});
} catch (error) {
return NextResponse.json({
success: false,
error: error instanceof Error ? error.message : 'Terjadi kesalahan'
}, { status: 500 });
}
}
2. Buat Komponen Report Page
/app/reports/page.tsx
tsx
Copy
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { format } from "date-fns";
async function getReportData() {
const res = await fetch('http://localhost:3000/api/reports', {
next: { revalidate: 3600 } // Revalidate setiap 1 jam
});
if (!res.ok) {
throw new Error('Gagal mengambil data laporan');
}
return res.json();
}
export default async function ReportsPage() {
const reportData = await getReportData();
if (!reportData.success) {
return <div>Error: {reportData.error}</div>;
}
const { topMenu, pendapatan, statusPembayaran, periode } = reportData.data;
const startDate = new Date(periode.startDate);
const endDate = new Date(periode.endDate);
return (
<div className="container mx-auto py-8">
<h1 className="text-2xl font-bold mb-6">Laporan Periode: {format(startDate, 'dd MMM yyyy')} - {format(endDate, 'dd MMM yyyy')}</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<Card>
<CardHeader>
<CardTitle>Total Pendapatan</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold">
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(pendapatan.total_pendapatan)}
</div>
<div className="text-sm text-muted-foreground mt-2">
<p>Lunas: {new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(pendapatan.detail.pendapatan_lunas)}</p>
<p>Laba Transaksi Kurang: {new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(pendapatan.detail.laba_transaksi_kurang)}</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Status Pembayaran</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{statusPembayaran.map((item: any) => (
<div key={item.payment_status} className="flex justify-between">
<span>{item.payment_status}:</span>
<span>{item.jumlah_transaksi} transaksi</span>
</div>
))}
</div>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>5 Menu Terlaris</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>No</TableHead>
<TableHead>Nama Menu</TableHead>
<TableHead className="text-right">Total Dipesan</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{topMenu.map((menu: any, index: number) => (
<TableRow key={menu.nama_menu || index}>
<TableCell>{index + 1}</TableCell>
<TableCell>{menu.nama_menu || 'Menu Tidak Diketahui'}</TableCell>
<TableCell className="text-right">{menu.total_dipesan || 0}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
}
3. Tambahkan Loading State (Optional)
Buat file /app/reports/loading.tsx untuk menampilkan skeleton loader:
tsx
Copy
import { Skeleton } from "@/components/ui/skeleton";
export default function Loading() {
return (
<div className="container mx-auto py-8 space-y-6">
<Skeleton className="h-8 w-64" />
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Skeleton className="h-40" />
<Skeleton className="h-40" />
</div>
<Skeleton className="h-96" />
</div>
);
}
4. Error Handling (Optional)
Buat file /app/reports/error.tsx untuk menangani error:
tsx
Copy
'use client';
import { useEffect } from "react";
import { Button } from "@/components/ui/button";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div className="container mx-auto py-8 text-center">
<h2 className="text-xl font-bold mb-4">Gagal memuat laporan</h2>
<p className="text-destructive mb-4">{error.message}</p>
<Button onClick={() => reset()}>Coba Lagi</Button>
</div>
);
}
jalan terakhir
'use client';
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { format } from "date-fns";
import { useEffect, useState } from "react";
export default function TopMenuPage() {
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const startDate = '2025-04-10';
const endDate = '2025-04-30';
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(
`http://localhost:8000/report/top-menu/?start_date=${startDate}&end_date=${endDate}`
);
if (!response.ok) {
throw new Error('Failed to fetch top menu data');
}
const result = await response.json();
if (result.success) {
setData(result.data || []);
} else {
throw new Error(result.error || 'Unknown error occurred');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'An unknown error occurred');
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (loading) {
return (
<div className="container mx-auto py-8">
<Card>
<CardHeader>
<CardTitle>5 Menu Terlaris</CardTitle>
<p className="text-sm text-muted-foreground">
Periode: {format(new Date(startDate), 'dd MMM yyyy')} - {format(new Date(endDate), 'dd MMM yyyy')}
</p>
</CardHeader>
<CardContent>
<div className="space-y-4">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-12 bg-muted/50 rounded animate-pulse"></div>
))}
</div>
</CardContent>
</Card>
</div>
);
}
if (error) {
return (
<div className="container mx-auto py-8 text-center">
<Card className="bg-destructive/10 border-destructive">
<CardHeader>
<CardTitle className="text-destructive">Error</CardTitle>
</CardHeader>
<CardContent>
<p>{error}</p>
<button
onClick={() => window.location.reload()}
className="mt-4 px-4 py-2 bg-primary text-primary-foreground rounded hover:bg-primary/90"
>
Coba Lagi
</button>
</CardContent>
</Card>
</div>
);
}
return (
<div className="container mx-auto py-8">
<Card>
<CardHeader>
<CardTitle>5 Menu Terlaris</CardTitle>
<p className="text-sm text-muted-foreground">
Periode: {format(new Date(startDate), 'dd MMM yyyy')} - {format(new Date(endDate), 'dd MMM yyyy')}
</p>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Peringkat</TableHead>
<TableHead>Menu</TableHead>
<TableHead className="text-right">Total Dipesan</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.length > 0 ? (
data.map((item, index) => (
<TableRow key={item.nama_menu || index}>
<TableCell>#{index + 1}</TableCell>
<TableCell className="font-medium">{item.nama_menu || 'Menu Tidak Diketahui'}</TableCell>
<TableCell className="text-right">{item.total_dipesan || 0}</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={3} className="text-center py-4">
Tidak ada data menu
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
}
top menu
"use client";
import { useEffect, useState } from "react";
interface MenuTerlaris {
menu_id__nama_menu: string;
total_dipesan: number;
}
export default function TopMenuTerlaris() {
const [data, setData] = useState<MenuTerlaris[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("http://localhost:8000/api/top-5-menu-terlaris/") // Ganti URL jika beda
.then((res) => res.json())
.then((json) => {
if (json.success) {
setData(json.data);
}
setLoading(false);
})
.catch((err) => {
console.error("Gagal ambil data:", err);
setLoading(false);
});
}, []);
return (
<div className="p-6 bg-white rounded-xl shadow-md w-full max-w-xl mx-auto mt-8">
<h2 className="text-2xl font-bold mb-4 text-center">Top 5 Menu Terlaris</h2>
{loading ? (
<p className="text-center">Memuat...</p>
) : data.length === 0 ? (
<p className="text-center">Belum ada data.</p>
) : (
<ul className="space-y-3">
{data.map((item, index) => (
<li
key={index}
className="flex justify-between bg-gray-100 p-3 rounded-lg hover:bg-gray-200 transition"
>
<span className="font-medium">{item.menu_id__nama_menu}</span>
<span className="text-sm text-gray-600">{item.total_dipesan}x</span>
</li>
))}
</ul>
)}
</div>
);
}
from rest_framework.decorators import api_view
from rest_framework.response import Response
from django.db.models import Sum
from .models import TransaksiDetail # atau sesuaikan path import kalau beda
@api_view(['GET'])
def top_5_menu_terlaris(request):
queryset = (
TransaksiDetail.objects
.values('menu_id__nama_menu') # gunakan double underscore untuk FK
.annotate(total_dipesan=Sum('qty'))
.order_by('-total_dipesan')[:5]
)
return Response({
'success': True,
'data': queryset,
'message': 'Data top 5 menu berhasil diambil'
})
Top comments (0)