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