DEV Community

Vytroo
Vytroo

Posted on • Edited on

Validasi Error & datepicker

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)

Enter fullscreen mode Exit fullscreen mode
"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&apos;t have an account?
            <Link href="#" className="ml-1 underline text-muted-foreground">
              Create account
            </Link>
          </p>
        </div>
      </div>
    </div>
  );
};

export default Login02Page;
Enter fullscreen mode Exit fullscreen mode

layout

import { Toaster } from "sonner";

// Di dalam komponen Layout Anda:
return (
  <html>
    <body>
      {/* ... konten lainnya ... */}
      <Toaster richColors position="top-center" />
    </body>
  </html>
);
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 <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>
  );
}
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<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>
  );
}
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<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'
    })

Enter fullscreen mode Exit fullscreen mode

Top comments (0)