Commit 6b40f710 authored by Ilham Maulana's avatar Ilham Maulana 💻

feat: frontend dashboard and books

parent a555a7a2
from django import forms
from .models import Book
"""
title = models.CharField(max_length=150)
author = models.CharField(max_length=50)
publish_date = models.DateTimeField()
rating = models.IntegerField(
default=0, validators=[MaxValueValidator(5), MinValueValidator(0)]
)
isbn = models.CharField(max_length=15, default="xxxxxxxxx-x")
description = models.CharField(max_length=255, blank=True, null=True)
cover_image = models.ImageField(upload_to="uploads", blank=True, null=True)
category = models.ManyToManyField(Category, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
"""
class BookForm(forms.ModelForm):
class Meta:
model = Book
fields = "__all__"
widgets = {
"cover_image": forms.FileInput(
attrs={
"placeholder": "Cover Image",
"class": "form-control",
}
),
"title": forms.TextInput(
attrs={
"placeholder": "Title",
"class": "form-control",
}
),
"author": forms.TextInput(
attrs={
"placeholder": "Author",
"class": "form-control",
}
),
"isbn": forms.TextInput(
attrs={
"placeholder": "ISBN",
"class": "form-control",
}
),
"rating": forms.TextInput(
attrs={
"type": "number",
"placeholder": "Rating",
"class": "form-control",
}
),
"category": forms.Select(
attrs={
"class": "form-control",
}
),
"description": forms.Textarea(
attrs={
"placeholder": "Description",
"class": "form-control",
}
),
"publish_date": forms.TextInput(
attrs={
"type": "number",
"class": "form-control",
}
),
}
{% extends "layout.html" %} {% block dashboard %}
<div style="max-width: 80vw" class="w-100 p-4">
<div class="d-flex justify-content-between pb-4">
<div class="d-flex gap-2 pb-4">
<a class="btn btn-success" href="/dashboard/books/{{ book.id }}/update">
<i class="bi bi-pencil-square"></i> Edit
</a>
<a class="btn btn-danger" href="/dashboard/books/{{ book.id }}/delete/">
<i class="bi bi-trash3-fill"></i> Delete
</a>
</div>
</div>
{% if book.cover_image %}
<div class="d-flex gap-5">
<img
class="object-fit-contain"
height="360"
src="{{ book.cover_image.url }}"
alt="{{ book.title }}"
/>
<div class="col">
<h1 class="h2 row">{{ book.title }}</h1>
<p class="h5 row">{{ book.description }}</p>
<p class="h5 row">Stock: {{ book.stock }}</p>
<p class="row badge text-bg-secondary">{{ book.category.name }}</p>
<time datetime="{{ book.created_at }}" class="row fs-6"
>Created at: {{ book.created_at }}</time
>
<time datetime="{{ book.updated_at }}" class="row fs-6"
>Updated at: {{ book.updated_at }}</time
>
</div>
</div>
{% endif %}
</div>
{% endblock dashboard %}
\ No newline at end of file
<table class="table table-hover">
<thead>
<tr class="table-primary">
<th scope="col">Title</th>
<th scope="col">Category</th>
<th scope="col">Stock</th>
<th scope="col">Description</th>
<th scope="col">Year</th>
<th scope="col">Created At</th>
<th scope="col">Updated At</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% if object_list %} {% for book in object_list %}
<tr>
<td><a href="/dashboard/books/{{ book.id }}/">{{ book.title }}</a></td>
<td>{{ book.category.name }}</td>
<td>{{ book.stock }}</td>
<td>{{ book.description }}</td>
<td>{{ book.published_year }}</td>
<td>{{ book.created_at }}</td>
<td>{{ book.updated_at }}</td>
<td>
<div class="d-flex gap-2">
<a
class="btn btn-success"
href="/dashboard/books/{{ book.id }}/update"
>
<i class="bi bi-pencil-square"></i>
</a>
<a
class="btn btn-danger"
href="/dashboard/books/{{ book.id }}/delete/"
>
<i class="bi bi-trash3-fill"></i>
</a>
</div>
</td>
</tr>
{% endfor %} {% else %}
<tr class="w-100">
<td></td>
<td></td>
<td></td>
<td>
<p>Data Empty</p>
</td>
<td></td>
<td></td>
<td></td>
</tr>
{% endif %}
</tbody>
</table>
{% include "pagination.html" %}
\ No newline at end of file
{% extends "layout.html" %} {% block dashboard %}
<div style="max-width: 80vw" class="w-100 p-4">
<div class="d-flex gap-2 pb-4">
{% include "order_form.html" %}
<a type="button" class="btn btn-primary" href="/dashboard/books/add">
<i class="bi bi-plus-circle"></i> Add Book
</a>
{% include "search_form.html" %}
</div>
{% include "book_table_data.html" %}
</div>
{% endblock dashboard %}
\ No newline at end of file
from django.urls import path
from .views import (
BookListView,
BookDetailView,
BookCreateView,
BookUpdateView,
BookDeleteView,
)
urlpatterns = [
path("", BookListView.as_view(), name="book_list"),
path("add/", BookCreateView.as_view(), name="book_add"),
path("<int:pk>/", BookDetailView.as_view(), name="book_detail"),
path("<int:pk>/update/", BookUpdateView.as_view(), name="book_update"),
path("<int:pk>/delete/", BookDeleteView.as_view(), name="book_delete"),
]
from django.shortcuts import render from django.db.models import Q
from django.views import generic
# Create your views here. from .models import Book
from .forms import BookForm
class BookListView(generic.ListView):
model = Book
template_name = "books.html"
paginate_by = 5
def get_queryset(self):
queryset = super().get_queryset()
keyword = self.request.GET.get("q")
order = self.request.GET.get("o")
if keyword:
queryset = queryset.filter(
Q(title__icontains=keyword)
| Q(category__name__icontains=keyword)
| Q(publish_date__year__icontains=keyword)
).order_by("-created_at")
if order:
if order == "new":
queryset = queryset.order_by("-created_at")
elif order == "old":
queryset = queryset.order_by("created_at")
return queryset
class BookDetailView(generic.DeleteView):
model = Book
template_name = "book_detail.html"
context_object_name = "book"
class BookCreateView(generic.edit.CreateView):
model = Book
form_class = BookForm
success_url = "/dashboard/books/"
template_name = "form/create_form.html"
class BookUpdateView(generic.edit.UpdateView):
model = Book
form_class = BookForm
success_url = "/dashboard/books"
template_name = "form/update_form.html"
class BookDeleteView(generic.edit.DeleteView):
model = Book
success_url = "/dashboard/books"
template_name = "form/delete_form.html"
...@@ -41,6 +41,7 @@ INSTALLED_APPS = [ ...@@ -41,6 +41,7 @@ INSTALLED_APPS = [
"users.apps.UsersConfig", "users.apps.UsersConfig",
"book.apps.BookConfig", "book.apps.BookConfig",
"loans.apps.LoansConfig", "loans.apps.LoansConfig",
"dashboard.apps.DashboardConfig",
# 3rd party # 3rd party
"rest_framework", "rest_framework",
"django_filters", "django_filters",
......
...@@ -21,6 +21,7 @@ from django.urls import path, include ...@@ -21,6 +21,7 @@ from django.urls import path, include
urlpatterns = [ urlpatterns = [
# local # local
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("dashboard/", include("dashboard.urls")),
# api # api
path("api/v1/", include("api.urls"), name="API_V1"), path("api/v1/", include("api.urls"), name="API_V1"),
# 3rd party # 3rd party
......
from django.contrib import admin
# Register your models here.
from django.apps import AppConfig
class DashboardConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'dashboard'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Library App</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
</head>
<body>
{% block content %}{% endblock content %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
</body>
</html>
\ No newline at end of file
{% extends "layout.html" %} {% block dashboard %}
<div style="max-width: 80vw;" class="w-100 p-4 d-flex flex-column gap-4">
<h1 class="h2">Reports</h1>
<div class="row card">{% include "dashboard/summary.html" %}</div>
<div class="row card">{% include "dashboard/overdue_loan.html" %}</div>
<div class="row card">{% include "dashboard/near_overdue_loan.html" %}</div>
<div class="row card">{% include "dashboard/login_history.html" %}</div>
</div>
{% endblock dashboard %}
<div class="card-header">
<h2 class="h4">Librarian Login History</h2>
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr class="table-primary">
<th scope="col">Name</th>
<th scope="col">Login At</th>
</tr>
</thead>
<tbody>
{% for histori in login_histories %}
<tr>
<td>{{ histori.librarian.name }}</td>
<td>{{ histori.login_at }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card-header d-flex justify-content-between">
<h2 class="h4">Near Outstanding Book Loan</h2>
<a href="/dashboard/upcoming-loans/" class="btn btn-primary"
>See All Upcoming Loans</a
>
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr class="table-primary">
<th scope="col">Book Title</th>
<th scope="col">Due Date</th>
<th scope="col">Loan Date</th>
</tr>
</thead>
<tbody>
{% if upcoming_loans %} {% for loan in upcoming_loans %}
<tr>
<td>{{ loan.book.title }}</td>
<td>{{ loan.due_date }}</td>
<td>{{ loan.loan_date }}</td>
</tr>
{% endfor %} {% else %}
<tr>
<td>No Data</td>
<td></td>
<td></td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<div class="card-header d-flex justify-content-between">
<h2 class="h4">Overdued Book Loan</h2>
<a href="/dashboard/overdued-loans/" class="btn btn-primary"
>See All Overdued Loans</a
>
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr class="table-primary">
<th scope="col">Book Title</th>
<th scope="col">Due Date</th>
<th scope="col">Loan Date</th>
</tr>
</thead>
<tbody>
{% if overdue_loans %} {% for loan in overdue_loans %}
<tr>
<td>{{ loan.book.title }}</td>
<td>{{ loan.due_date }}</td>
<td>{{ loan.loan_date }}</td>
</tr>
{% endfor %} {% else %}
<tr>
<td>No Data</td>
<td></td>
<td></td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<div class="card-header">
<h3 class="h4">At a Glance</h3>
</div>
<div class="card-body row container text-center">
<div class="col">
<div class="card h-100 d-flex flex-column">
<div class="card-body row align-content-center">
<h5 class="card-title"><i class="bi bi-book-half"></i> Total Book</h5>
<p class="h1">{{ total_book }}</p>
</div>
<div class="card-footer">
<a href="/dashboard/books/" class="w-100 btn btn-primary"
>Explore Book</a
>
</div>
</div>
</div>
<div class="col">
<div class="card h-100 d-flex flex-column">
<div class="card-body row align-content-center">
<h5 class="card-title">
<i class="bi bi-book-half"></i> Total Category
</h5>
<p class="h1">{{ total_category }}</p>
</div>
<div class="card-footer">
<a href="/dashboard/books/" class="w-100 btn btn-primary"
>Explore Categories</a
>
</div>
</div>
</div>
<div class="col">
<div class="card h-100">
<div class="card-body row align-content-center">
<h5 class="card-title">
<i class="bi bi-person-vcard"></i> Total Member
</h5>
<p class="h1">{{ total_member }}</p>
</div>
<div class="card-footer">
<a href="/dashboard/members/" class="w-100 btn btn-primary"
>Go to Member</a
>
</div>
</div>
</div>
<div class="col">
<div class="card h-100">
<div class="card-body row align-content-center">
<h5 class="card-title">
<i class="bi bi-calendar-week"></i> Book Loan
</h5>
<div class="d-flex justify-content-center gap-2">
<div class="card">
<div class="card-header">Total</div>
<p class="h1">{{ total_book_loans }}</p>
</div>
<div class="card">
<div class="card-header">Upcoming</div>
<p class="h1">{{ total_upcoming }}</p>
</div>
<div class="card">
<div class="card-header">Overdue</div>
<p class="h1">{{ total_overdue }}</p>
</div>
</div>
</div>
<div class="card-footer">
<a href="/dashboard/book-loans/" class="w-100 btn btn-primary"
>Go to Book Loans</a
>
</div>
</div>
</div>
</div>
{% extends "layout.html" %} {% block dashboard %}
<div style="max-width: 80vw" class="w-100 p-4">
<div class="d-flex flex-column gap-2 mb-4">
<h1 class="h3">Add Data</h1>
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
<div class="d-flex flex-column gap-1">{{ form }}</div>
<div class="d-flex gap-2 my-3">
<a href="javascript:window.history.back()" class="btn btn-secondary"
>Cancel</a
>
<button class="btn btn-primary">Submit</button>
</div>
</form>
</div>
</div>
{% endblock dashboard %}
{% extends "layout.html" %} {% block dashboard %}
<div style="max-width: 80vw" class="w-100 p-4">
<div class="d-flex flex-column gap-2 mb-4">
<h1 class="h3">Are you sure want to delete this data
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
<div class="d-flex flex-column gap-1">Once data is deleted, it cannot be restored.</div>
<div class="d-flex gap-2 my-3">
<a href="javascript:window.history.back()" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-danger">Continue</button>
</div>
</form>
</div>
</div>
{% endblock dashboard %}
{% extends "layout.html" %} {% block dashboard %}
<div style="max-width: 80vw" class="w-100 p-4">
<div class="d-flex flex-column gap-2 mb-4">
<h1 class="h3">Update Data</h1>
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
<div class="d-flex flex-column gap-1">{{ form }}</div>
<div class="d-flex gap-2 my-3">
<a href="javascript:window.history.back()" class="btn btn-secondary"
>Cancel</a
>
<button class="btn btn-primary">Submit</button>
</div>
</form>
</div>
</div>
{% endblock dashboard %}
{% extends "base.html" %} {% block content %}
<main
style="min-height: 100vh"
class="w-100 h-100 bg-body-secondary d-flex flex-column justify-content-center align-items-center"
>
<h2 class="h3">Welcome to</h2>
<h1 class="h1">Django Library Management System</h1>
<section
class="d-flex flex-column justify-content-center align-items-center my-3 w-100"
>
<h4 class="h4">Let's get started</h4>
<div class="d-flex gap-2 my-2">
<a href="/auth/login" class="btn btn-outline-primary">Login</a>
<a href="/auth/sign-up" class="btn btn-primary">Sign Up</a>
</div>
</section>
</main>
{% endblock content %}
{% extends "base.html" %} {% block content %}
<main
style="min-height: 100vh"
class="w-100 h-100 bg-body-secondary d-flex justify-content-end"
>
<div
class="w-100 p-4 d-flex flex-column gap-4 bg-dark"
style="max-width: 20vw"
>
{% include "profile.html" %}
<div class="d-flex flex-column gap-2">
<a
href="/dashboard"
class="btn {% if request.path == '/dashboard/' %}btn-primary{% else %}btn-outline-primary border-white text-white{% endif %} text-start w-100"
><i class="bi bi-columns-gap"></i> Dashboard</a
>
<a
href="/dashboard/books"
class="btn {% if request.path == '/dashboard/books/' %}btn-primary{% else %}btn-outline-primary border-white text-white{% endif %} text-start w-100"
><i class="bi bi-book-half"></i> Books</a
>
<a
href="/dashboard/categories"
class="btn {% if request.path == '/dashboard/categories/' %}btn-primary{% else %}btn-outline-primary border-white text-white{% endif %} text-start w-100"
><i class="bi bi-tags-fill"></i> Categories</a
>
<a
href="/dashboard/members"
class="btn {% if request.path == '/dashboard/members/' %}btn-primary{% else %}btn-outline-primary border-white text-white{% endif %} text-start w-100"
><i class="bi bi-person-vcard"></i> Members</a
>
<a
href="/dashboard/librarians"
class="btn {% if request.path == '/dashboard/librarians/' %}btn-primary{% else %}btn-outline-primary border-white text-white{% endif %} text-start w-100"
><i class="bi bi-person-fill-lock"></i> Librarian</a
>
<a
href="/dashboard/book-loans"
class="btn {% if request.path == '/dashboard/book-loans/' %}btn-primary{% else %}btn-outline-primary border-white text-white{% endif %} text-start w-100"
><i class="bi bi-calendar-week"></i> Book Loans</a
>
<a
href="/dashboard/upcoming-loans/"
class="btn {% if request.path == '/dashboard/upcoming-loans/' %}btn-primary{% else %}btn-outline-primary border-white text-white{% endif %} text-start w-100"
><i class="bi bi-calendar2-event"></i> Upcoming Loans</a
>
<a
href="/dashboard/overdued-loans/"
class="btn {% if request.path == '/dashboard/overdued-loans/' %}btn-primary{% else %}btn-outline-primary border-white text-white{% endif %} text-start w-100"
><i class="bi bi-calendar2-x"></i> Overdued Loans</a
>
</div>
</div>
<div style="max-height: 100vh" class="w-100 overflow-y-auto">
{% block dashboard %}{% endblock dashboard %}
</div>
</main>
{% endblock content %}
<div class="dropdown">
<button
type="button"
class="btn btn-secondary dropdown-toggle"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i class="bi bi-arrow-down-up"></i> Order By
</button>
<ul class="dropdown-menu shadow">
<li>
<form action="" method="get" class="d-flex gap-2">
<input name="o" value="new" hidden />
<button type="submit" class="dropdown-item" href="#">Newest</button>
</form>
</li>
<li>
<form action="" method="get" class="d-flex gap-2">
<input name="o" value="old" hidden />
<button type="submit" class="dropdown-item" href="#">Oldest</button>
</form>
</li>
</ul>
</div>
{% if is_paginated %}
<nav aria-label="Page navigation example">
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}"
>Previous</a
>
</li>
{% else %}
<li class="page-item">
<a class="page-link disabled" href="#">Previous</a>
</li>
{% endif %} {% for index in page_obj.paginator.page_range %}
<li class="page-item {% if page_obj.number == index %}active{% endif %}">
<a class="page-link" href="?page={{ index }}">{{ index }}</a>
</li>
{% endfor %} {% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}"
>Next</a
>
</li>
{% else %}
<li class="page-item">
<a class="page-link disabled" href="#">Next</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
<div class="d-flex flex-column gap-2">
<button
class="w-100 btn btn-primary text-start d-flex justify-content-center gap-2"
type="button"
data-bs-toggle="dropdown"
>
<i class="bi bi-person-circle"></i>
{% if user %} <span class="text-truncate">{{ user.name }}</span> {% endif %}
</button>
<a
class="w-100 btn btn-outline-primary border-white text-white text-start"
href="/dashboard/librarians/{{ user.id }}/"
><i class="bi bi-person-fill-gear"></i> profile</a
>
<a
class="w-100 btn btn-outline-primary border-white text-white text-start"
href="/auth/logout/{{ user.id }}/"
><i class="bi bi-box-arrow-left"></i> logout</a
>
</div>
<hr class="border-white" />
<form action="" method="get" class="input-group w-50">
<input
type="text"
name="q"
class="form-control"
placeholder="Search..."
aria-label="Search..."
/>
<button class="input-group-text btn btn-primary">Search</button>
</form>
from django.test import TestCase
# Create your tests here.
from django.urls import path, include
from dashboard.views import DashboardView
urlpatterns = [
path("", DashboardView.as_view(), name="dashboard"),
path("books/", include("book.urls")),
# path("categories/", include("categories.urls")),
# path("members/", include("members.urls")),
# path("book-loans/", include("book_loans.urls")),
# path("librarians/", include("librarians.urls")),
# path("upcoming-loans/", UpcomingLoanView.as_view(), name="upcoming_loans"),
# path("overdued-loans/", OverduedLoanView.as_view(), name="overdued_loans"),
]
from django.utils import timezone
from django.db.models import Q
from django.views.generic import ListView, TemplateView
from users.models import LibrarianLoginHistory, Member
from book.models import Category, Book
from loans.models import BookLoan
class OverduedLoanView(ListView):
model = BookLoan
template_name = "loans.html"
paginate_by = 5
def get_queryset(self):
queryset = super().get_queryset()
keyword = self.request.GET.get("q")
order = self.request.GET.get("o")
now = timezone.now()
queryset = queryset.filter(due_date__lte=now, return_date=None).order_by(
"-due_date"
)
if keyword:
queryset = queryset.filter(
Q(book__title__icontains=keyword)
| Q(member__name__icontains=keyword)
| Q(librarian__name__icontains=keyword)
).order_by("-due_date")
if order:
if order == "new":
queryset = queryset.order_by("-due_date")
elif order == "old":
queryset = queryset.order_by("due_date")
return queryset
class UpcomingLoanView(ListView):
model = BookLoan
template_name = "loans.html"
paginate_by = 5
def get_queryset(self):
queryset = super().get_queryset()
keyword = self.request.GET.get("q")
order = self.request.GET.get("o")
now = timezone.now()
due_date_treshold = now + timezone.timedelta(days=3)
queryset = (
queryset.filter(due_date__lte=due_date_treshold, return_date=None)
.filter(due_date__gte=now)
.order_by("-due_date")
)
if keyword:
queryset = queryset.filter(
Q(book__title__icontains=keyword)
| Q(member__name__icontains=keyword)
| Q(librarian__name__icontains=keyword)
).order_by("-due_date")
if order:
if order == "new":
queryset = queryset.order_by("-due_date")
elif order == "old":
queryset = queryset.order_by("due_date")
return queryset
class HomePage(TemplateView):
template_name = "homepage.html"
class DashboardView(TemplateView):
template_name = "dashboard/index.html"
login_history = LibrarianLoginHistory.objects.order_by("-date")[:10]
book_loans = BookLoan.objects.all()
total_book = Book.objects.count()
total_category = Category.objects.count()
total_member = Member.objects.count()
total_book_loans = book_loans.count()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
now = timezone.now()
overdue_loans = self.book_loans.filter(
due_date__lte=now, return_date=None
).order_by("-due_date")[:10]
due_date_treshold = now + timezone.timedelta(days=3)
upcoming_loans = (
self.book_loans.filter(due_date__lte=due_date_treshold, return_date=None)
.filter(due_date__gte=now)
.order_by("-due_date")[:10]
)
context["login_histories"] = self.login_history
context["total_book"] = self.total_book
context["total_category"] = self.total_category
context["total_member"] = self.total_member
context["total_book_loans"] = self.total_book_loans
context["total_overdue"] = overdue_loans.count()
context["total_upcoming"] = upcoming_loans.count()
context["overdue_loans"] = overdue_loans
context["upcoming_loans"] = upcoming_loans
return context
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment