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>({
defaultValues: {
username: "",
password: "",
},
resolver: zodResolver(formSchema),
});
const onSubmit = async (data: z.infer) => {
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 (
Log in to Restoran SMK
(
Username
)}
/>
(
Password
)}
/>
Login
Forgot your password?
Don't have an account?
Create account
);
};
export default Login02Page;
layout
import { Toaster } from "sonner";
// Di dalam komponen Layout Anda:
return (
{/* ... konten lainnya ... */}
);
Enter fullscreen mode
Exit fullscreen mode
# 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)
Enter fullscreen mode
Exit fullscreen mode
reporting
npm install @tanstack/react-table date-fns
Enter fullscreen mode
Exit fullscreen mode
/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 Error: {reportData.error};
}
const { topMenu, pendapatan, statusPembayaran, periode } = reportData.data;
const startDate = new Date(periode.startDate);
const endDate = new Date(periode.endDate);
return (
Laporan Periode: {format(startDate, 'dd MMM yyyy')} - {format(endDate, 'dd MMM yyyy')}
Total Pendapatan
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(pendapatan.total_pendapatan)}
Lunas: {new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(pendapatan.detail.pendapatan_lunas)}
Laba Transaksi Kurang: {new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(pendapatan.detail.laba_transaksi_kurang)}
Status Pembayaran
{statusPembayaran.map((item: any) => (
{item.payment_status}:
{item.jumlah_transaksi} transaksi
))}
5 Menu Terlaris
No
Nama Menu
Total Dipesan
{topMenu.map((menu: any, index: number) => (
{index + 1}
{menu.nama_menu || 'Menu Tidak Diketahui'}
{menu.total_dipesan || 0}
))}
);
}
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 (
);
}
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 (
Gagal memuat laporan
{error.message}
reset()}>Coba Lagi
);
}
Enter fullscreen mode
Exit fullscreen mode
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([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(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 (
5 Menu Terlaris
Periode: {format(new Date(startDate), 'dd MMM yyyy')} - {format(new Date(endDate), 'dd MMM yyyy')}
{[...Array(5)].map((_, i) => (
))}
);
}
if (error) {
return (
Error
{error}
window.location.reload()}
className="mt-4 px-4 py-2 bg-primary text-primary-foreground rounded hover:bg-primary/90"
>
Coba Lagi
);
}
return (
5 Menu Terlaris
Periode: {format(new Date(startDate), 'dd MMM yyyy')} - {format(new Date(endDate), 'dd MMM yyyy')}
Peringkat
Menu
Total Dipesan
{data.length > 0 ? (
data.map((item, index) => (
#{index + 1}
{item.nama_menu || 'Menu Tidak Diketahui'}
{item.total_dipesan || 0}
))
) : (
Tidak ada data menu
)}
);
}
Enter fullscreen mode
Exit fullscreen mode
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([]);
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 (
Top 5 Menu Terlaris
{loading ? (
Memuat...
) : data.length === 0 ? (
Belum ada data.
) : (
{data.map((item, index) => (
{item.menu_id__nama_menu}
{item.total_dipesan}x
))}
)}
);
}
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'
})
Enter fullscreen mode
Exit fullscreen mode