Autosave: 20260206-112737

This commit is contained in:
Flatlogic Bot 2026-02-06 11:27:38 +00:00
parent 7af198c681
commit 28bc475346
40 changed files with 2473 additions and 185 deletions

View File

@ -133,9 +133,9 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = 'id'
TIME_ZONE = 'UTC'
TIME_ZONE = 'Asia/Jakarta'
USE_I18N = True
@ -180,3 +180,5 @@ if EMAIL_USE_SSL:
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/accounts/login/'

View File

@ -21,9 +21,10 @@ from django.conf.urls.static import static
urlpatterns = [
path("admin/", admin.site.urls),
path("accounts/", include("django.contrib.auth.urls")),
path("", include("core.urls")),
]
if settings.DEBUG:
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

Binary file not shown.

View File

@ -1,3 +1,39 @@
from django.contrib import admin
from .models import Category, Medicine, Batch, StockTransaction, Supplier, Faktur
# Register your models here.
# DN-WRS Branding
admin.site.site_header = "DN-WRS Admin"
admin.site.site_title = "DN-WRS Portal"
admin.site.index_title = "Selamat Datang di Manajemen DN-WRS"
@admin.register(Supplier)
class SupplierAdmin(admin.ModelAdmin):
list_display = ('name', 'contact_person', 'phone')
search_fields = ('name',)
@admin.register(Faktur)
class FakturAdmin(admin.ModelAdmin):
list_display = ('faktur_number', 'supplier', 'date', 'faktur_type')
list_filter = ('faktur_type', 'date')
search_fields = ('faktur_number',)
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name',)
@admin.register(Medicine)
class MedicineAdmin(admin.ModelAdmin):
list_display = ('name', 'category', 'sku', 'unit', 'total_stock', 'status')
search_fields = ('name', 'sku')
list_filter = ('category', 'unit')
@admin.register(Batch)
class BatchAdmin(admin.ModelAdmin):
list_display = ('medicine', 'batch_number', 'expiry_date', 'quantity', 'is_expired')
list_filter = ('expiry_date',)
search_fields = ('batch_number', 'medicine__name')
@admin.register(StockTransaction)
class StockTransactionAdmin(admin.ModelAdmin):
list_display = ('medicine', 'transaction_type', 'quantity', 'created_at')
list_filter = ('transaction_type', 'created_at')

107
core/forms.py Normal file
View File

@ -0,0 +1,107 @@
from django import forms
from .models import Supplier, Faktur, Medicine, Batch, StockTransaction, Category
class CategoryForm(forms.ModelForm):
class Meta:
model = Category
fields = ['name', 'description']
widgets = {
'name': forms.TextInput(attrs={'class': 'form-control'}),
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
class SupplierForm(forms.ModelForm):
class Meta:
model = Supplier
fields = ['name', 'contact_person', 'phone', 'address']
widgets = {
'name': forms.TextInput(attrs={'class': 'form-control'}),
'contact_person': forms.TextInput(attrs={'class': 'form-control'}),
'phone': forms.TextInput(attrs={'class': 'form-control'}),
'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
class MedicineForm(forms.ModelForm):
class Meta:
model = Medicine
fields = ['name', 'category', 'sku', 'unit', 'min_stock', 'main_supplier', 'alternative_supplier']
widgets = {
'name': forms.TextInput(attrs={'class': 'form-control'}),
'category': forms.Select(attrs={'class': 'form-control'}),
'sku': forms.TextInput(attrs={'class': 'form-control'}),
'unit': forms.Select(attrs={'class': 'form-control'}),
'min_stock': forms.NumberInput(attrs={'class': 'form-control'}),
'main_supplier': forms.Select(attrs={'class': 'form-control'}),
'alternative_supplier': forms.Select(attrs={'class': 'form-control'}),
}
class FakturForm(forms.ModelForm):
class Meta:
model = Faktur
fields = ['faktur_number', 'supplier', 'date', 'faktur_type', 'notes']
widgets = {
'faktur_number': forms.TextInput(attrs={'class': 'form-control'}),
'supplier': forms.Select(attrs={'class': 'form-control'}),
'date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'faktur_type': forms.Select(attrs={'class': 'form-control'}),
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
}
class StockInForm(forms.Form):
medicine = forms.ModelChoiceField(
queryset=Medicine.objects.all(),
label="Nama Barang",
widget=forms.Select(attrs={'class': 'form-control select2'})
)
batch_number = forms.CharField(
label="Nomor Batch/Lot",
widget=forms.TextInput(attrs={'class': 'form-control'})
)
expiry_date = forms.DateField(
label="Tanggal Kadaluarsa",
widget=forms.DateInput(attrs={'class': 'form-control', 'type': 'date'})
)
quantity = forms.IntegerField(
label="Jumlah",
widget=forms.NumberInput(attrs={'class': 'form-control'})
)
buying_price = forms.DecimalField(
label="Harga Beli",
widget=forms.NumberInput(attrs={'class': 'form-control'})
)
selling_price = forms.DecimalField(
label="Harga Jual",
widget=forms.NumberInput(attrs={'class': 'form-control'})
)
class StockOutForm(forms.Form):
medicine = forms.ModelChoiceField(
queryset=Medicine.objects.all(),
label="Pilih Barang",
widget=forms.Select(attrs={'class': 'form-control select2', 'id': 'id_medicine_out'})
)
batch = forms.ModelChoiceField(
queryset=Batch.objects.none(),
label="Pilih Batch",
widget=forms.Select(attrs={'class': 'form-control', 'id': 'id_batch_out'})
)
quantity = forms.IntegerField(
label="Jumlah Keluar",
widget=forms.NumberInput(attrs={'class': 'form-control'})
)
note = forms.CharField(
label="Keterangan",
required=False,
widget=forms.TextInput(attrs={'class': 'form-control'})
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'medicine' in self.data:
try:
medicine_id = int(self.data.get('medicine'))
self.fields['batch'].queryset = Batch.objects.filter(medicine_id=medicine_id, quantity__gt=0)
except (ValueError, TypeError):
pass
elif self.initial.get('medicine'):
self.fields['batch'].queryset = Batch.objects.filter(medicine_id=self.initial.get('medicine'), quantity__gt=0)

View File

@ -0,0 +1,64 @@
# Generated by Django 5.2.7 on 2026-02-06 09:38
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Category',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('description', models.TextField(blank=True)),
],
options={
'verbose_name_plural': 'Categories',
},
),
migrations.CreateModel(
name='Medicine',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('sku', models.CharField(max_length=50, unique=True)),
('unit', models.CharField(choices=[('Tablet', 'Tablet'), ('Kapsul', 'Kapsul'), ('Botol', 'Botol'), ('Strip', 'Strip'), ('Pcs', 'Pcs'), ('Box', 'Box')], default='Tablet', max_length=20)),
('min_stock', models.IntegerField(default=10)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='medicines', to='core.category')),
],
),
migrations.CreateModel(
name='Batch',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('batch_number', models.CharField(max_length=100)),
('expiry_date', models.DateField()),
('quantity', models.IntegerField(default=0)),
('buying_price', models.DecimalField(decimal_places=2, max_digits=12)),
('selling_price', models.DecimalField(decimal_places=2, max_digits=12)),
('created_at', models.DateTimeField(auto_now_add=True)),
('medicine', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='batches', to='core.medicine')),
],
),
migrations.CreateModel(
name='StockTransaction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('transaction_type', models.CharField(choices=[('IN', 'Barang Masuk'), ('OUT', 'Barang Keluar'), ('ADJ', 'Penyesuaian')], max_length=3)),
('quantity', models.IntegerField()),
('note', models.TextField(blank=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('batch', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.batch')),
('medicine', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.medicine')),
],
),
]

View File

@ -0,0 +1,170 @@
# Generated by Django 5.2.7 on 2026-02-06 10:18
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Faktur',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('faktur_number', models.CharField(max_length=100, unique=True, verbose_name='Nomor Faktur')),
('date', models.DateField(default=django.utils.timezone.now, verbose_name='Tanggal')),
('faktur_type', models.CharField(choices=[('MASUK', 'Faktur Masuk (Supplier)'), ('KELUAR', 'Faktur Keluar')], default='MASUK', max_length=10, verbose_name='Tipe Faktur')),
('notes', models.TextField(blank=True, verbose_name='Catatan')),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={
'verbose_name': 'Faktur',
'verbose_name_plural': 'Faktur',
},
),
migrations.CreateModel(
name='Supplier',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Nama Supplier')),
('contact_person', models.CharField(blank=True, max_length=100, verbose_name='Kontak Person')),
('phone', models.CharField(blank=True, max_length=20, verbose_name='Telepon')),
('address', models.TextField(blank=True, verbose_name='Alamat')),
],
options={
'verbose_name': 'Supplier',
'verbose_name_plural': 'Supplier',
},
),
migrations.AlterModelOptions(
name='batch',
options={'verbose_name': 'Batch Barang', 'verbose_name_plural': 'Batch Barang'},
),
migrations.AlterModelOptions(
name='category',
options={'verbose_name': 'Kategori', 'verbose_name_plural': 'Kategori'},
),
migrations.AlterModelOptions(
name='medicine',
options={'verbose_name': 'Barang/Obat', 'verbose_name_plural': 'Barang/Obat'},
),
migrations.AlterModelOptions(
name='stocktransaction',
options={'verbose_name': 'Transaksi Stok', 'verbose_name_plural': 'Transaksi Stok'},
),
migrations.AlterField(
model_name='batch',
name='batch_number',
field=models.CharField(max_length=100, verbose_name='Nomor Batch/Lot'),
),
migrations.AlterField(
model_name='batch',
name='buying_price',
field=models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Harga Beli'),
),
migrations.AlterField(
model_name='batch',
name='expiry_date',
field=models.DateField(verbose_name='Tanggal Kadaluarsa'),
),
migrations.AlterField(
model_name='batch',
name='medicine',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='batches', to='core.medicine', verbose_name='Barang'),
),
migrations.AlterField(
model_name='batch',
name='quantity',
field=models.IntegerField(default=0, verbose_name='Jumlah Stok'),
),
migrations.AlterField(
model_name='batch',
name='selling_price',
field=models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Harga Jual'),
),
migrations.AlterField(
model_name='category',
name='description',
field=models.TextField(blank=True, verbose_name='Deskripsi'),
),
migrations.AlterField(
model_name='category',
name='name',
field=models.CharField(max_length=100, verbose_name='Nama Kategori'),
),
migrations.AlterField(
model_name='medicine',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='medicines', to='core.category', verbose_name='Kategori'),
),
migrations.AlterField(
model_name='medicine',
name='min_stock',
field=models.IntegerField(default=10, verbose_name='Stok Minimal'),
),
migrations.AlterField(
model_name='medicine',
name='name',
field=models.CharField(max_length=255, verbose_name='Nama Barang'),
),
migrations.AlterField(
model_name='medicine',
name='sku',
field=models.CharField(max_length=50, unique=True, verbose_name='SKU/Kode'),
),
migrations.AlterField(
model_name='medicine',
name='unit',
field=models.CharField(choices=[('Tablet', 'Tablet'), ('Kapsul', 'Kapsul'), ('Botol', 'Botol'), ('Strip', 'Strip'), ('Pcs', 'Pcs'), ('Box', 'Box')], default='Tablet', max_length=20, verbose_name='Satuan'),
),
migrations.AlterField(
model_name='stocktransaction',
name='batch',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transactions', to='core.batch', verbose_name='Batch'),
),
migrations.AlterField(
model_name='stocktransaction',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='Waktu'),
),
migrations.AlterField(
model_name='stocktransaction',
name='medicine',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.medicine', verbose_name='Barang'),
),
migrations.AlterField(
model_name='stocktransaction',
name='note',
field=models.TextField(blank=True, verbose_name='Keterangan'),
),
migrations.AlterField(
model_name='stocktransaction',
name='quantity',
field=models.IntegerField(verbose_name='Jumlah'),
),
migrations.AlterField(
model_name='stocktransaction',
name='transaction_type',
field=models.CharField(choices=[('IN', 'Barang Masuk'), ('OUT', 'Barang Keluar'), ('ADJ', 'Penyesuaian')], max_length=3, verbose_name='Tipe Transaksi'),
),
migrations.AddField(
model_name='batch',
name='faktur',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='batches', to='core.faktur', verbose_name='Faktur'),
),
migrations.AddField(
model_name='stocktransaction',
name='faktur',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transactions', to='core.faktur', verbose_name='Faktur'),
),
migrations.AddField(
model_name='faktur',
name='supplier',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.supplier', verbose_name='Supplier'),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 5.2.7 on 2026-02-06 10:56
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_faktur_supplier_alter_batch_options_and_more'),
]
operations = [
migrations.AddField(
model_name='medicine',
name='alternative_supplier',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='alternative_medicines', to='core.supplier', verbose_name='Supplier Alternatif'),
),
migrations.AddField(
model_name='medicine',
name='main_supplier',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_medicines', to='core.supplier', verbose_name='Supplier Utama'),
),
]

View File

@ -1,3 +1,134 @@
from django.db import models
from django.utils import timezone
# Create your models here.
class Category(models.Model):
name = models.CharField(max_length=100, verbose_name="Nama Kategori")
description = models.TextField(blank=True, verbose_name="Deskripsi")
def __str__(self):
return self.name
class Meta:
verbose_name_plural = "Kategori"
verbose_name = "Kategori"
class Supplier(models.Model):
name = models.CharField(max_length=255, verbose_name="Nama Supplier")
contact_person = models.CharField(max_length=100, blank=True, verbose_name="Kontak Person")
phone = models.CharField(max_length=20, blank=True, verbose_name="Telepon")
address = models.TextField(blank=True, verbose_name="Alamat")
def __str__(self):
return self.name
class Meta:
verbose_name_plural = "Supplier"
verbose_name = "Supplier"
class Medicine(models.Model):
UNIT_CHOICES = [
('Tablet', 'Tablet'),
('Kapsul', 'Kapsul'),
('Botol', 'Botol'),
('Strip', 'Strip'),
('Pcs', 'Pcs'),
('Box', 'Box'),
]
name = models.CharField(max_length=255, verbose_name="Nama Barang")
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='medicines', verbose_name="Kategori")
sku = models.CharField(max_length=50, unique=True, verbose_name="SKU/Kode")
unit = models.CharField(max_length=20, choices=UNIT_CHOICES, default='Tablet', verbose_name="Satuan")
min_stock = models.IntegerField(default=10, verbose_name="Stok Minimal")
main_supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, blank=True, related_name='primary_medicines', verbose_name="Supplier Utama")
alternative_supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, blank=True, related_name='alternative_medicines', verbose_name="Supplier Alternatif")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
@property
def total_stock(self):
return sum(batch.quantity for batch in self.batches.all())
@property
def status(self):
total = self.total_stock
if total <= 0:
return "Habis"
if total <= self.min_stock:
return "Stok Menipis"
return "Tersedia"
class Meta:
verbose_name_plural = "Barang/Obat"
verbose_name = "Barang/Obat"
class Faktur(models.Model):
FAKTUR_TYPE_CHOICES = [
('MASUK', 'Faktur Masuk (Supplier)'),
('KELUAR', 'Faktur Keluar'),
]
faktur_number = models.CharField(max_length=100, unique=True, verbose_name="Nomor Faktur")
supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Supplier")
date = models.DateField(default=timezone.now, verbose_name="Tanggal")
faktur_type = models.CharField(max_length=10, choices=FAKTUR_TYPE_CHOICES, default='MASUK', verbose_name="Tipe Faktur")
notes = models.TextField(blank=True, verbose_name="Catatan")
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.faktur_number
class Meta:
verbose_name_plural = "Faktur"
verbose_name = "Faktur"
class Batch(models.Model):
medicine = models.ForeignKey(Medicine, on_delete=models.CASCADE, related_name='batches', verbose_name="Barang")
faktur = models.ForeignKey(Faktur, on_delete=models.SET_NULL, null=True, blank=True, related_name='batches', verbose_name="Faktur")
batch_number = models.CharField(max_length=100, verbose_name="Nomor Batch/Lot")
expiry_date = models.DateField(verbose_name="Tanggal Kadaluarsa")
quantity = models.IntegerField(default=0, verbose_name="Jumlah Stok")
buying_price = models.DecimalField(max_digits=12, decimal_places=2, verbose_name="Harga Beli")
selling_price = models.DecimalField(max_digits=12, decimal_places=2, verbose_name="Harga Jual")
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.medicine.name} - {self.batch_number}"
@property
def is_expired(self):
return self.expiry_date <= timezone.now().date()
@property
def is_near_expiry(self):
today = timezone.now().date()
diff = self.expiry_date - today
return 0 < diff.days <= 90 # 3 months
class Meta:
verbose_name_plural = "Batch Barang"
verbose_name = "Batch Barang"
class StockTransaction(models.Model):
TRANSACTION_TYPES = [
('IN', 'Barang Masuk'),
('OUT', 'Barang Keluar'),
('ADJ', 'Penyesuaian'),
]
medicine = models.ForeignKey(Medicine, on_delete=models.CASCADE, verbose_name="Barang")
batch = models.ForeignKey(Batch, on_delete=models.SET_NULL, null=True, blank=True, related_name='transactions', verbose_name="Batch")
faktur = models.ForeignKey(Faktur, on_delete=models.SET_NULL, null=True, blank=True, related_name='transactions', verbose_name="Faktur")
transaction_type = models.CharField(max_length=3, choices=TRANSACTION_TYPES, verbose_name="Tipe Transaksi")
quantity = models.IntegerField(verbose_name="Jumlah")
note = models.TextField(blank=True, verbose_name="Keterangan")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Waktu")
def __str__(self):
return f"{self.get_transaction_type_display()} - {self.medicine.name} ({self.quantity})"
class Meta:
verbose_name_plural = "Transaksi Stok"
verbose_name = "Transaksi Stok"

View File

@ -1,25 +1,128 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<html lang="id">
<head>
<meta charset="UTF-8">
<title>{% block title %}Knowledge Base{% endblock %}</title>
{% if project_description %}
<meta name="description" content="{{ project_description }}">
<meta property="og:description" content="{{ project_description }}">
<meta property="twitter:description" content="{{ project_description }}">
{% endif %}
{% if project_image_url %}
<meta property="og:image" content="{{ project_image_url }}">
<meta property="twitter:image" content="{{ project_image_url }}">
{% endif %}
{% load static %}
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
{% block head %}{% endblock %}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ project_name|default:"DN-WRS" }}</title>
<!-- Bootstrap 5 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Google Fonts: Plus Jakarta Sans -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- Custom CSS -->
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={% now 'U' %}">
{% block extra_head %}{% endblock %}
</head>
<body>
{% block content %}{% endblock %}
</body>
{% if user.is_authenticated %}
<nav class="navbar navbar-expand-lg navbar-custom sticky-top py-3 mb-4">
<div class="container">
<a class="navbar-brand fw-bold text-primary d-flex align-items-center" href="{% url 'home' %}">
<i data-lucide="package" class="me-2"></i>
<span>{{ project_name|default:"DN-WRS" }}</span>
</a>
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<i data-lucide="menu"></i>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link px-3 d-flex align-items-center {% if request.resolver_match.url_name == 'home' %}active{% endif %}" href="{% url 'home' %}">
<i data-lucide="layout-dashboard" class="me-2 icon-sm"></i> Beranda
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link px-3 dropdown-toggle d-flex align-items-center" href="#" role="button" data-bs-toggle="dropdown">
<i data-lucide="database" class="me-2 icon-sm"></i> Master Data
</a>
<ul class="dropdown-menu border-0 shadow-lg rounded-4 p-2">
<li><a class="dropdown-item rounded-3" href="{% url 'medicine_list' %}"><i data-lucide="pill" class="me-2 icon-sm"></i> Data Barang</a></li>
<li><a class="dropdown-item rounded-3" href="{% url 'category_list' %}"><i data-lucide="tag" class="me-2 icon-sm"></i> Kategori</a></li>
<li><a class="dropdown-item rounded-3" href="{% url 'supplier_list' %}"><i data-lucide="truck" class="me-2 icon-sm"></i> Supplier</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link px-3 d-flex align-items-center {% if request.resolver_match.url_name == 'input_faktur' %}active{% endif %}" href="{% url 'input_faktur' %}">
<i data-lucide="file-plus" class="me-2 icon-sm"></i> Faktur Masuk
</a>
</li>
<li class="nav-item">
<a class="nav-link px-3 d-flex align-items-center {% if request.resolver_match.url_name == 'barang_keluar' %}active{% endif %}" href="{% url 'barang_keluar' %}">
<i data-lucide="file-minus" class="me-2 icon-sm"></i> Barang Keluar
</a>
</li>
<li class="nav-item">
<a class="nav-link px-3 d-flex align-items-center {% if request.resolver_match.url_name == 'laporan_transaksi' %}active{% endif %}" href="{% url 'laporan_transaksi' %}">
<i data-lucide="bar-chart-2" class="me-2 icon-sm"></i> Laporan
</a>
</li>
</ul>
<div class="d-flex align-items-center">
<div class="dropdown">
<button class="btn btn-link text-dark text-decoration-none dropdown-toggle d-flex align-items-center" type="button" id="userDropdown" data-bs-toggle="dropdown">
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px; font-size: 0.8rem;">
{{ user.username|slice:":1"|upper }}
</div>
<span class="fw-semibold">{{ user.username }}</span>
</button>
<ul class="dropdown-menu dropdown-menu-end border-0 shadow-lg rounded-4 p-2">
<li><a class="dropdown-item rounded-3 py-2" href="/admin/password_change/">Ubah Password</a></li>
<li><hr class="dropdown-divider"></li>
<li>
<form method="post" action="{% url 'logout' %}">
{% csrf_token %}
<button type="submit" class="dropdown-item rounded-3 py-2 text-danger">Keluar</button>
</form>
</li>
</ul>
</div>
</div>
</div>
</div>
</nav>
{% endif %}
</html>
<div class="container">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show rounded-4 border-0 shadow-sm mb-4" role="alert">
<div class="d-flex align-items-center">
<i data-lucide="{% if message.tags == 'success' %}check-circle{% else %}alert-circle{% endif %}" class="me-2"></i>
{{ message }}
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
</div>
<main class="container py-2 fade-in">
{% block content %}{% endblock %}
</main>
<footer class="container py-5 text-center text-muted small mt-auto">
<p class="mb-0">&copy; {% now "Y" %} {{ project_name|default:"DN-WRS" }}</p>
<p class="opacity-50">Sistem Manajemen Gudang Premium</p>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Initialize Lucide icons
lucide.createIcons();
// Custom icon sizes
document.querySelectorAll('.icon-sm').forEach(el => {
el.setAttribute('width', '18');
el.setAttribute('height', '18');
});
</script>
{% block extra_js %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,152 @@
{% extends 'base.html' %}
{% block content %}
<div class="row g-4">
<div class="col-md-4">
<div class="card glass-card border-0 shadow-sm">
<div class="card-body p-4">
<h4 class="fw-bold mb-4 d-flex align-items-center">
<div class="bg-danger-subtle text-danger p-2 rounded-3 me-3">
<i data-lucide="file-minus"></i>
</div>
Barang Keluar
</h4>
<form method="post" id="outForm">
{% csrf_token %}
<div class="mb-3">
<label class="form-label small fw-bold text-muted text-uppercase mb-1" style="font-size: 0.65rem; letter-spacing: 0.5px;">PILIH BARANG</label>
{{ form.medicine }}
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted text-uppercase mb-1" style="font-size: 0.65rem; letter-spacing: 0.5px;">PILIH BATCH (STOK TERSEDIA)</label>
{{ form.batch }}
<div id="stock-hint" class="small text-primary mt-2 fw-bold"></div>
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted text-uppercase mb-1" style="font-size: 0.65rem; letter-spacing: 0.5px;">JUMLAH KELUAR</label>
{{ form.quantity }}
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted text-uppercase mb-1" style="font-size: 0.65rem; letter-spacing: 0.5px;">KETERANGAN</label>
{{ form.note }}
</div>
<button type="submit" class="btn btn-danger w-100 py-3 mt-3 shadow-sm d-flex align-items-center justify-content-center">
<i data-lucide="minus-circle" class="me-2 icon-sm"></i> Catat Barang Keluar
</button>
</form>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card glass-card border-0 shadow-sm h-100">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-bold mb-0 d-flex align-items-center">
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-3">
<i data-lucide="history"></i>
</div>
Transaksi Terakhir
</h4>
<span class="badge bg-light text-dark border px-3 py-2 rounded-pill">10 Transaksi</span>
</div>
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Barang</th>
<th>Batch</th>
<th class="text-end">Jumlah</th>
<th>Waktu</th>
<th class="text-end">Aksi</th>
</tr>
</thead>
<tbody>
{% for t in transactions %}
<tr>
<td>
<div class="fw-bold text-dark">{{ t.medicine.name }}</div>
<div class="small text-muted">{{ t.note|default:"-" }}</div>
</td>
<td><code class="text-primary">{{ t.batch.batch_number }}</code></td>
<td class="text-end fw-bold text-danger">-{{ t.quantity }}</td>
<td><span class="small text-muted">{{ t.created_at|date:"d/m/y H:i" }}</span></td>
<td class="text-end">
<a href="{% url 'edit_transaksi' t.pk %}" class="btn btn-light btn-sm rounded-circle p-2">
<i data-lucide="edit-3" class="icon-sm"></i>
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center py-5 text-muted">
<i data-lucide="inbox" class="mb-2 d-block mx-auto opacity-20" style="width: 48px; height: 48px;"></i>
Belum ada transaksi keluar.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_head %}
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<style>
.select2-container--default .select2-selection--single {
border: 1px solid #e2e8f0;
height: 48px;
border-radius: 0.75rem;
padding-top: 10px;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 46px;
}
</style>
{% endblock %}
{% block extra_js %}
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(document).ready(function() {
$('#id_medicine_out').select2({
placeholder: "Pilih Barang",
allowClear: true
});
$('#id_medicine_out').on('change', function() {
var medicineId = $(this).val();
var batchSelect = $('#id_batch_out');
batchSelect.empty().append('<option value="">Memuat...</option>');
if (medicineId) {
$.ajax({
url: "{% url 'get_batches' %}",
data: { 'medicine_id': medicineId },
success: function(data) {
batchSelect.empty().append('<option value="">Pilih Batch</option>');
data.forEach(function(batch) {
batchSelect.append('<option value="' + batch.id + '" data-qty="' + batch.quantity + '">' + batch.batch_number + ' (Stok: ' + batch.quantity + ')</option>');
});
}
});
} else {
batchSelect.empty().append('<option value="">Pilih Barang Terlebih Dahulu</option>');
}
});
$('#id_batch_out').on('change', function() {
var selected = $(this).find('option:selected');
var qty = selected.data('qty');
if (qty !== undefined) {
$('#stock-hint').text('Stok tersedia: ' + qty);
} else {
$('#stock-hint').text('');
}
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,83 @@
{% extends 'base.html' %}
{% block content %}
<div class="row g-4">
<div class="col-md-4">
<div class="card glass-card border-0 shadow-sm">
<div class="card-body p-4">
<h4 class="fw-bold mb-4 d-flex align-items-center">
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-3">
<i data-lucide="tag"></i>
</div>
Tambah Kategori
</h4>
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
<label class="form-label small fw-bold text-muted text-uppercase mb-1" style="font-size: 0.65rem; letter-spacing: 0.5px;">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}
<div class="form-text small">{{ field.help_text }}</div>
{% endif %}
{% for error in field.errors %}
<div class="text-danger small">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<button type="submit" class="btn btn-primary w-100 py-3 mt-3 shadow-sm d-flex align-items-center justify-content-center">
<i data-lucide="save" class="me-2 icon-sm"></i> Simpan Kategori
</button>
</form>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card glass-card border-0 shadow-sm">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-bold mb-0 d-flex align-items-center">
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-3">
<i data-lucide="layers"></i>
</div>
Daftar Kategori
</h4>
<span class="badge bg-light text-dark border px-3 py-2 rounded-pill">{{ categories|length }} Total</span>
</div>
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Nama Kategori</th>
<th>Deskripsi</th>
<th class="text-end">Aksi</th>
</tr>
</thead>
<tbody>
{% for c in categories %}
<tr>
<td>
<div class="fw-bold text-dark">{{ c.name }}</div>
</td>
<td><span class="text-muted small">{{ c.description|default:"-"|truncatechars:100 }}</span></td>
<td class="text-end">
<a href="/admin/core/category/{{ c.id }}/change/" class="btn btn-light btn-sm rounded-circle p-2">
<i data-lucide="edit-3" class="icon-sm"></i>
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-center py-5 text-muted">
<i data-lucide="inbox" class="mb-2 d-block mx-auto opacity-20" style="width: 48px; height: 48px;"></i>
Belum ada data kategori.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,61 @@
{% extends 'base.html' %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card glass-card border-0 shadow-sm">
<div class="card-body p-4 p-lg-5">
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'laporan_transaksi' %}" class="text-decoration-none">Laporan</a></li>
<li class="breadcrumb-item active" aria-current="page">Edit Transaksi</li>
</ol>
</nav>
<h3 class="fw-bold mb-4 d-flex align-items-center">
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-3">
<i data-lucide="edit-3"></i>
</div>
Edit Transaksi
</h3>
<div class="p-4 bg-light rounded-4 mb-4 border border-white">
<div class="row g-3">
<div class="col-sm-6">
<small class="text-muted d-block text-uppercase fw-bold mb-1" style="font-size: 0.65rem; letter-spacing: 0.5px;">BARANG</small>
<span class="fw-bold text-dark">{{ transaction.medicine.name }}</span>
</div>
<div class="col-sm-6 text-sm-end">
<small class="text-muted d-block text-uppercase fw-bold mb-1" style="font-size: 0.65rem; letter-spacing: 0.5px;">TIPE</small>
<span class="badge {% if transaction.transaction_type == 'IN' %}bg-success-subtle text-success{% else %}bg-danger-subtle text-danger{% endif %} rounded-pill px-3">
{{ transaction.get_transaction_type_display }}
</span>
</div>
<div class="col-12 mt-3">
<small class="text-muted d-block text-uppercase fw-bold mb-1" style="font-size: 0.65rem; letter-spacing: 0.5px;">BATCH NUMBER</small>
<code class="text-primary fs-6">{{ transaction.batch.batch_number }}</code>
</div>
</div>
</div>
<form method="post">
{% csrf_token %}
<div class="mb-4">
<label class="form-label small fw-bold text-muted text-uppercase mb-1" style="font-size: 0.65rem; letter-spacing: 0.5px;">JUMLAH</label>
<input type="number" name="quantity" class="form-control form-control-lg rounded-3" value="{{ transaction.quantity }}" required>
<div class="form-text small mt-2">Perubahan jumlah akan otomatis memperbarui stok pada batch.</div>
</div>
<div class="mb-4">
<label class="form-label small fw-bold text-muted text-uppercase mb-1" style="font-size: 0.65rem; letter-spacing: 0.5px;">KETERANGAN</label>
<textarea name="note" class="form-control rounded-3" rows="3">{{ transaction.note }}</textarea>
</div>
<div class="d-flex gap-3 mt-5">
<a href="{% url 'laporan_transaksi' %}" class="btn btn-light flex-grow-1 py-3 fw-bold">Batal</a>
<button type="submit" class="btn btn-primary flex-grow-1 py-3 fw-bold shadow-sm">Simpan Perubahan</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,131 @@
{% extends 'base.html' %}
{% block content %}
<div class="mb-4">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'input_faktur' %}" class="text-decoration-none">Riwayat Faktur</a></li>
<li class="breadcrumb-item active" aria-current="page">Detail Faktur {{ faktur.faktur_number }}</li>
</ol>
</nav>
<div class="card glass-card border-0 shadow-sm p-4 mb-2">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted small fw-bold text-uppercase mb-1" style="letter-spacing: 1px;">NOMOR FAKTUR</h6>
<h2 class="fw-bold mb-0 text-primary">#{{ faktur.faktur_number }}</h2>
</div>
<div class="text-end">
<h6 class="text-muted small fw-bold text-uppercase mb-1" style="letter-spacing: 1px;">SUPPLIER</h6>
<div class="badge bg-primary-subtle text-primary rounded-pill px-4 py-2 fs-6 fw-bold">
<i data-lucide="truck" class="me-2 icon-sm"></i> {{ faktur.supplier.name|default:"Internal" }}
</div>
</div>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-md-4">
<div class="card glass-card border-0 shadow-sm">
<div class="card-body p-4">
<h5 class="fw-bold mb-4 d-flex align-items-center">
<div class="bg-success-subtle text-success p-2 rounded-3 me-3">
<i data-lucide="package-plus"></i>
</div>
Input Barang
</h5>
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
<label class="form-label small fw-bold text-muted text-uppercase mb-1" style="font-size: 0.65rem; letter-spacing: 0.5px;">{{ field.label }}</label>
{{ field }}
{% if field.errors %}
<div class="text-danger small">{{ field.errors }}</div>
{% endif %}
</div>
{% endfor %}
<button type="submit" class="btn btn-primary w-100 py-3 mt-3 shadow-sm d-flex align-items-center justify-content-center">
<i data-lucide="plus" class="me-2 icon-sm"></i> Tambah Barang
</button>
</form>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card glass-card border-0 shadow-sm h-100">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="fw-bold mb-0 d-flex align-items-center">
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-3">
<i data-lucide="list"></i>
</div>
Rincian Barang
</h5>
<span class="badge bg-light text-dark border px-3 py-2 rounded-pill">{{ items|length }} Item</span>
</div>
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Barang</th>
<th>No. Batch</th>
<th>Kadaluarsa</th>
<th class="text-end">Jumlah</th>
<th class="text-end">Harga Beli</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>
<div class="fw-bold text-dark">{{ item.medicine.name }}</div>
<div class="small text-muted">{{ item.medicine.sku }}</div>
</td>
<td><code class="text-primary">{{ item.batch_number }}</code></td>
<td><span class="small text-muted">{{ item.expiry_date|date:"d M Y" }}</span></td>
<td class="text-end fw-bold text-dark">{{ item.quantity }} <span class="text-muted small">{{ item.medicine.unit }}</span></td>
<td class="text-end text-success fw-semibold">Rp {{ item.buying_price|floatformat:2 }}</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center py-5 text-muted">
<i data-lucide="package-search" class="mb-2 d-block mx-auto opacity-20" style="width: 48px; height: 48px;"></i>
<p class="mb-0">Belum ada barang diinput untuk faktur ini.</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_head %}
<!-- Select2 CSS -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<style>
.select2-container--default .select2-selection--single {
border: 1px solid #e2e8f0;
height: 48px;
border-radius: 0.75rem;
padding-top: 10px;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 46px;
}
</style>
{% endblock %}
{% block extra_js %}
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(document).ready(function() {
$('.select2').select2({
placeholder: "Pilih Barang",
allowClear: true
});
});
</script>
{% endblock %}

View File

@ -1,145 +1,260 @@
{% extends "base.html" %}
{% block title %}{{ project_name }}{% endblock %}
{% block head %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% {
background-position: 0% 0%;
}
100% {
background-position: 100% 100%;
}
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2.5rem 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
}
h1 {
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
font-weight: 700;
margin: 0 0 1.2rem;
letter-spacing: -0.02em;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
opacity: 0.92;
}
.loader {
margin: 1.5rem auto;
width: 56px;
height: 56px;
border: 4px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.runtime code {
background: rgba(0, 0, 0, 0.25);
padding: 0.15rem 0.45rem;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
footer {
position: absolute;
bottom: 1rem;
width: 100%;
text-align: center;
font-size: 0.85rem;
opacity: 0.75;
}
</style>
{% endblock %}
{% load static %}
{% block content %}
<main>
<div class="card">
<h1>Analyzing your requirements and generating your app…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
<div class="row g-4 mb-4">
<!-- Stats Cards -->
<div class="col-md-3">
<div class="card glass-card h-100">
<div class="card-body">
<div class="stats-icon bg-primary-subtle text-primary">
<i data-lucide="package"></i>
</div>
<h6 class="text-muted mb-1 small fw-bold text-uppercase">TOTAL BARANG</h6>
<h2 class="fw-bold mb-0 text-dark">{{ total_medicines }}</h2>
</div>
</div>
</div>
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
<p class="runtime">
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
</p>
</div>
</main>
<footer>
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
</footer>
{% endblock %}
<div class="col-md-3">
<div class="card glass-card h-100">
<div class="card-body">
<div class="stats-icon bg-success-subtle text-success">
<i data-lucide="boxes"></i>
</div>
<h6 class="text-muted mb-1 small fw-bold text-uppercase">TOTAL STOK</h6>
<h2 class="fw-bold mb-0 text-dark">{{ total_stock }}</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card glass-card h-100">
<div class="card-body">
<div class="stats-icon bg-warning-subtle text-warning">
<i data-lucide="alert-triangle"></i>
</div>
<h6 class="text-muted mb-1 small fw-bold text-uppercase">STOK MENIPIS</h6>
<h2 class="fw-bold mb-0 text-dark">{{ low_stock_count }}</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card glass-card h-100">
<div class="card-body">
<div class="stats-icon bg-danger-subtle text-danger">
<i data-lucide="calendar-x"></i>
</div>
<h6 class="text-muted mb-1 small fw-bold text-uppercase">KADALUARSA</h6>
<h2 class="fw-bold mb-0 text-dark">{{ expired_count }}</h2>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-4">
<!-- Chart Area -->
<div class="col-lg-8">
<div class="card glass-card h-100">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="fw-bold mb-0">Pergerakan Stok</h5>
<span class="badge bg-light text-dark border py-2 px-3 rounded-pill small">7 Hari Terakhir</span>
</div>
<div style="height: 300px;">
<canvas id="movementChart"></canvas>
</div>
</div>
</div>
</div>
<!-- Expiry Overview -->
<div class="col-lg-4">
<div class="card border-0 rounded-4 h-100 bg-white shadow-sm overflow-hidden">
<div class="card-body p-0">
<div class="p-4 bg-danger text-white">
<h5 class="fw-bold mb-0">Pantauan Kadaluarsa</h5>
<p class="small opacity-75 mb-0">Segera kadaluarsa (90 hari ke depan)</p>
</div>
<div class="p-4 text-center">
<div class="display-1 fw-bold text-danger mb-2">{{ near_expiry_count }}</div>
<p class="text-muted mb-4">Item memerlukan perhatian segera</p>
<div class="d-grid">
<a href="/admin/core/batch/" class="btn btn-outline-danger d-flex align-items-center justify-content-center">
<i data-lucide="external-link" class="me-2 icon-sm"></i> Tinjau Batch
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-12">
<div class="card glass-card">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="fw-bold mb-0 d-flex align-items-center">
<i data-lucide="history" class="me-2 text-primary"></i> Data Barang Terbaru
</h5>
<a href="/admin/core/medicine/" class="btn btn-primary btn-sm d-flex align-items-center px-3 rounded-pill">
<i data-lucide="settings" class="me-2 icon-sm"></i> Kelola Semua
</a>
</div>
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead>
<tr>
<th>SKU</th>
<th>Nama Barang</th>
<th>Kategori</th>
<th>Stok</th>
<th>Status</th>
<th class="text-end">Aksi</th>
</tr>
</thead>
<tbody>
{% for medicine in recent_medicines %}
<tr>
<td><span class="badge bg-light text-muted">{{ medicine.sku }}</span></td>
<td><span class="fw-bold">{{ medicine.name }}</span></td>
<td><span class="text-muted small">{{ medicine.category.name }}</span></td>
<td>
<span class="fw-bold">{{ medicine.total_stock }}</span>
<span class="text-muted small ms-1">{{ medicine.unit }}</span>
</td>
<td>
{% if medicine.status == 'Tersedia' %}
<span class="badge bg-success-subtle text-success rounded-pill px-3">Tersedia</span>
{% elif medicine.status == 'Stok Menipis' %}
<span class="badge bg-warning-subtle text-warning rounded-pill px-3">Stok Menipis</span>
{% else %}
<span class="badge bg-danger-subtle text-danger rounded-pill px-3">Habis</span>
{% endif %}
</td>
<td class="text-end">
<a href="/admin/core/medicine/{{ medicine.id }}/change/" class="btn btn-light btn-sm rounded-circle p-2">
<i data-lucide="edit-3" class="icon-sm"></i>
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center py-5 text-muted">
<i data-lucide="inbox" class="mb-2 d-block mx-auto opacity-20" style="width: 48px; height: 48px;"></i>
Belum ada data tersedia.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
const ctx = document.getElementById('movementChart').getContext('2d');
// Create gradients
const gradientIn = ctx.createLinearGradient(0, 0, 0, 300);
gradientIn.addColorStop(0, 'rgba(67, 97, 238, 0.2)');
gradientIn.addColorStop(1, 'rgba(67, 97, 238, 0)');
const gradientOut = ctx.createLinearGradient(0, 0, 0, 300);
gradientOut.addColorStop(0, 'rgba(239, 35, 60, 0.2)');
gradientOut.addColorStop(1, 'rgba(239, 35, 60, 0)');
new Chart(ctx, {
type: 'line',
data: {
labels: {{ chart_labels|safe }},
datasets: [
{
label: 'Barang Masuk',
data: {{ chart_data_in|safe }},
borderColor: '#4361ee',
backgroundColor: gradientIn,
fill: true,
tension: 0.4,
borderWidth: 3,
pointRadius: 4,
pointHoverRadius: 6,
pointBackgroundColor: '#fff',
pointBorderWidth: 2
},
{
label: 'Barang Keluar',
data: {{ chart_data_out|safe }},
borderColor: '#ef233c',
backgroundColor: gradientOut,
fill: true,
tension: 0.4,
borderWidth: 3,
pointRadius: 4,
pointHoverRadius: 6,
pointBackgroundColor: '#fff',
pointBorderWidth: 2
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false,
},
plugins: {
legend: {
position: 'top',
align: 'end',
labels: {
usePointStyle: true,
boxWidth: 8,
font: {
family: 'Plus Jakarta Sans',
size: 12,
weight: '600'
}
}
},
tooltip: {
padding: 12,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
titleColor: '#1e293b',
titleFont: { size: 14, weight: 'bold' },
bodyColor: '#64748b',
borderColor: '#e2e8f0',
borderWidth: 1,
displayColors: true,
boxPadding: 6,
usePointStyle: true
}
},
scales: {
y: {
beginAtZero: true,
grid: {
color: 'rgba(0, 0, 0, 0.03)',
drawBorder: false
},
ticks: {
font: { family: 'Plus Jakarta Sans' }
}
},
x: {
grid: {
display: false
},
ticks: {
font: { family: 'Plus Jakarta Sans' }
}
}
}
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,86 @@
{% extends 'base.html' %}
{% block content %}
<div class="row g-4">
<div class="col-md-4">
<div class="card glass-card border-0 shadow-sm h-100">
<div class="card-body p-4">
<h4 class="fw-bold mb-4 d-flex align-items-center">
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-3">
<i data-lucide="file-plus"></i>
</div>
Input Faktur Baru
</h4>
<form method="post">
{% csrf_token %}
{% for field in faktur_form %}
<div class="mb-3">
<label class="form-label small fw-bold text-muted text-uppercase mb-1" style="font-size: 0.65rem; letter-spacing: 0.5px;">{{ field.label }}</label>
{{ field }}
{% if field.errors %}
<div class="text-danger small">{{ field.errors }}</div>
{% endif %}
</div>
{% endfor %}
<button type="submit" class="btn btn-primary w-100 py-3 mt-3 shadow-sm d-flex align-items-center justify-content-center">
<i data-lucide="arrow-right" class="me-2 icon-sm"></i> Simpan & Lanjutkan
</button>
</form>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card glass-card border-0 shadow-sm h-100">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-bold mb-0 d-flex align-items-center">
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-3">
<i data-lucide="history"></i>
</div>
Riwayat Faktur
</h4>
<span class="badge bg-light text-dark border px-3 py-2 rounded-pill">{{ fakturs|length }} Total</span>
</div>
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>No. Faktur</th>
<th>Supplier</th>
<th>Tanggal</th>
<th>Tipe</th>
<th class="text-end">Aksi</th>
</tr>
</thead>
<tbody>
{% for f in fakturs %}
<tr>
<td class="fw-bold text-dark">{{ f.faktur_number }}</td>
<td>{{ f.supplier.name|default:"-" }}</td>
<td><span class="text-muted small">{{ f.date }}</span></td>
<td>
<span class="badge {% if f.faktur_type == 'MASUK' %}bg-info-subtle text-info{% else %}bg-warning-subtle text-warning{% endif %} rounded-pill px-3">
{{ f.get_faktur_type_display }}
</span>
</td>
<td class="text-end">
<a href="{% url 'faktur_detail' f.pk %}" class="btn btn-light btn-sm rounded-pill px-3 fw-bold">
<i data-lucide="plus-circle" class="me-1 icon-sm"></i> Input Barang
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center py-5 text-muted">
<i data-lucide="inbox" class="mb-2 d-block mx-auto opacity-20" style="width: 48px; height: 48px;"></i>
Belum ada data faktur.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,122 @@
{% extends 'base.html' %}
{% block content %}
<div class="card glass-card border-0 shadow-sm mb-4">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h3 class="fw-bold mb-0 d-flex align-items-center">
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-3">
<i data-lucide="bar-chart-2"></i>
</div>
Laporan Keluar Masuk Barang
</h3>
<div class="d-flex gap-2">
<button class="btn btn-outline-primary rounded-pill px-4 fw-bold" onclick="window.print()">
<i data-lucide="printer" class="icon-sm me-2"></i> Cetak Laporan
</button>
</div>
</div>
<div class="table-responsive">
<table class="table align-middle" id="reportTable">
<thead>
<tr>
<th>Waktu</th>
<th>Barang</th>
<th>Tipe</th>
<th class="text-end">Jumlah</th>
<th>Keterangan</th>
<th class="text-end">Aksi</th>
</tr>
</thead>
<tbody>
{% for t in transactions %}
<tr>
<td>
<div class="small fw-bold text-dark">{{ t.created_at|date:"d M Y" }}</div>
<div class="small text-muted">{{ t.created_at|date:"H:i" }} WIB</div>
</td>
<td>
<div class="fw-bold text-dark">{{ t.medicine.name }}</div>
<div class="small text-primary fw-medium">Batch: {{ t.batch.batch_number|default:"-" }}</div>
</td>
<td>
{% if t.transaction_type == 'IN' %}
<span class="badge bg-success-subtle text-success rounded-pill px-3">Masuk</span>
{% elif t.transaction_type == 'OUT' %}
<span class="badge bg-danger-subtle text-danger rounded-pill px-3">Keluar</span>
{% else %}
<span class="badge bg-secondary-subtle text-secondary rounded-pill px-3">Penyesuaian</span>
{% endif %}
</td>
<td class="text-end fw-bold fs-5">
{% if t.transaction_type == 'OUT' %}
<span class="text-danger">-{{ t.quantity }}</span>
{% else %}
<span class="text-success">+{{ t.quantity }}</span>
{% endif %}
</td>
<td><span class="small text-muted">{{ t.note|default:"-" }}</span></td>
<td class="text-end">
<div class="d-flex justify-content-end gap-1">
<a href="{% url 'edit_transaksi' t.pk %}" class="btn btn-light btn-sm rounded-circle p-2" title="Edit">
<i data-lucide="edit-3" class="icon-sm"></i>
</a>
<button type="button" class="btn btn-light btn-sm rounded-circle p-2" title="Hapus"
onclick="confirmDelete('{{ t.pk }}', '{{ t.medicine.name }}')">
<i data-lucide="trash-2" class="icon-sm text-danger"></i>
</button>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center py-5 text-muted">
<i data-lucide="inbox" class="mb-2 d-block mx-auto opacity-20" style="width: 48px; height: 48px;"></i>
Belum ada transaksi stok.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Modal Konfirmasi Hapus -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">Konfirmasi Hapus</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body py-4 text-center">
<div class="bg-danger-subtle text-danger p-3 rounded-circle d-inline-flex mb-3">
<i data-lucide="alert-triangle" style="width: 32px; height: 32px;"></i>
</div>
<h5>Hapus Transaksi?</h5>
<p class="text-muted">Apakah Anda yakin ingin menghapus transaksi <span id="deleteItemName" class="fw-bold text-dark"></span>? Tindakan ini akan mengembalikan stok pada batch terkait.</p>
</div>
<div class="modal-footer border-0 pt-0 justify-content-center pb-4">
<button type="button" class="btn btn-light rounded-pill px-4 fw-bold" data-bs-dismiss="modal">Batal</button>
<form id="deleteForm" method="post" action="">
{% csrf_token %}
<button type="submit" class="btn btn-danger rounded-pill px-4 fw-bold shadow-sm">Ya, Hapus Transaksi</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
function confirmDelete(pk, name) {
document.getElementById('deleteItemName').innerText = name;
document.getElementById('deleteForm').action = "/transaksi/" + pk + "/delete/";
var myModal = new bootstrap.Modal(document.getElementById('deleteModal'));
myModal.show();
}
</script>
{% endblock %}

View File

@ -0,0 +1,238 @@
{% extends 'base.html' %}
{% block content %}
<div class="row g-4 mb-4">
<div class="col-md-4">
<div class="card glass-card border-0 shadow-sm h-100">
<div class="card-body p-4 text-center">
<div class="bg-primary-subtle text-primary p-3 rounded-circle d-inline-flex mb-3">
<i data-lucide="package" style="width: 24px; height: 24px;"></i>
</div>
<h6 class="text-muted fw-bold mb-1">Total Barang</h6>
<h2 class="fw-bold mb-0 text-dark">{{ total_count }}</h2>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card glass-card border-0 shadow-sm h-100">
<div class="card-body p-4 text-center">
<div class="bg-warning-subtle text-warning p-3 rounded-circle d-inline-flex mb-3">
<i data-lucide="alert-triangle" style="width: 24px; height: 24px;"></i>
</div>
<h6 class="text-muted fw-bold mb-1">Stok Menipis</h6>
<h2 class="fw-bold mb-0 text-warning">{{ low_stock_count }}</h2>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card glass-card border-0 shadow-sm h-100">
<div class="card-body p-4 text-center">
<div class="bg-success-subtle text-success p-3 rounded-circle d-inline-flex mb-3">
<i data-lucide="check-circle" style="width: 24px; height: 24px;"></i>
</div>
<h6 class="text-muted fw-bold mb-1">Status Aman</h6>
<h2 class="fw-bold mb-0 text-success">{{ total_count|add:"-"|add:low_stock_count|default:0|cut:"-" }}</h2>
</div>
</div>
</div>
</div>
<div class="card glass-card border-0 shadow-sm mb-4">
<div class="card-body p-4">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-4 gap-3">
<h3 class="fw-bold mb-0 d-flex align-items-center text-dark">
<div class="bg-primary text-white p-2 rounded-3 me-3">
<i data-lucide="pill"></i>
</div>
Manajemen Data Barang
</h3>
<div class="d-flex flex-wrap gap-2">
<button type="button" class="btn btn-primary rounded-pill px-4 fw-bold shadow-sm d-flex align-items-center" data-bs-toggle="modal" data-bs-target="#addMedicineModal">
<i data-lucide="plus-circle" class="me-2 icon-sm"></i> Tambah Barang Baru
</button>
<a href="{% url 'export_low_stock_pdf' %}" class="btn btn-outline-danger rounded-pill px-4 fw-bold d-flex align-items-center">
<i data-lucide="file-down" class="me-2 icon-sm"></i> Export Stok Menipis (PDF)
</a>
</div>
</div>
<hr class="opacity-10 mb-4">
<!-- Search & Filters -->
<div class="row g-3 mb-4 align-items-center">
<div class="col-lg-6">
<form method="GET" class="d-flex gap-2">
<div class="input-group flex-grow-1">
<span class="input-group-text bg-white border-end-0 border-opacity-25 rounded-start-pill ps-3"><i data-lucide="search" class="icon-sm text-muted"></i></span>
<input type="text" name="q" class="form-control border-start-0 border-opacity-25 rounded-end-pill py-2" placeholder="Cari nama, SKU, atau kategori..." value="{{ query|default:'' }}">
</div>
{% if query %}
<a href="{% url 'medicine_list' %}" class="btn btn-light rounded-pill px-3 d-flex align-items-center">
<i data-lucide="x" class="icon-sm"></i>
</a>
{% endif %}
<button type="submit" class="btn btn-secondary rounded-pill px-4 fw-bold">Cari</button>
</form>
</div>
<div class="col-lg-6 d-flex justify-content-lg-end gap-2">
<div class="btn-group p-1 bg-light rounded-pill" role="group">
<a href="{% url 'medicine_list' %}{% if query %}?q={{ query }}{% endif %}" class="btn rounded-pill px-4 fw-bold {% if not show_low_stock %}btn-white shadow-sm{% else %}btn-transparent text-muted{% endif %}">
Semua
</a>
<a href="{% url 'medicine_list' %}?low_stock=1{% if query %}&q={{ query }}{% endif %}" class="btn rounded-pill px-4 fw-bold {% if show_low_stock %}btn-warning shadow-sm{% else %}btn-transparent text-muted{% endif %}">
Stok Menipis
{% if low_stock_count > 0 %}
<span class="badge rounded-pill bg-danger ms-1">{{ low_stock_count }}</span>
{% endif %}
</a>
</div>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light opacity-75">
<tr>
<th class="ps-3">SKU</th>
<th>Nama Barang</th>
<th>Kategori</th>
<th>Supplier Utama</th>
<th>Supplier Alternatif</th>
<th class="text-center">Stok</th>
<th>Status</th>
<th class="text-end pe-3">Aksi</th>
</tr>
</thead>
<tbody class="border-top-0">
{% for m in medicines %}
<tr>
<td class="ps-3"><span class="badge bg-light text-muted border fw-normal">{{ m.sku }}</span></td>
<td>
<div class="fw-bold text-dark">{{ m.name }}</div>
<div class="text-muted small d-flex align-items-center">
<i data-lucide="box" class="icon-xs me-1"></i> {{ m.unit }}
</div>
</td>
<td>
<span class="badge bg-info-subtle text-info rounded-pill px-2 small">{{ m.category.name }}</span>
</td>
<td>
<div class="small fw-semibold text-dark">{{ m.main_supplier.name|default:"-" }}</div>
{% if m.main_supplier %}
<div class="text-muted smaller"><i data-lucide="phone" class="icon-xs me-1"></i> {{ m.main_supplier.phone|default:"N/A" }}</div>
{% endif %}
</td>
<td>
<div class="small text-muted">{{ m.alternative_supplier.name|default:"-" }}</div>
</td>
<td class="text-center">
<div class="fs-5 fw-bold {% if m.total_stock <= m.min_stock %}text-danger{% else %}text-primary{% endif %}">
{{ m.total_stock }}
</div>
<div class="text-muted small">Min: {{ m.min_stock }}</div>
</td>
<td>
{% if m.status == 'Tersedia' %}
<span class="badge bg-success-subtle text-success rounded-pill px-3 py-2 d-inline-flex align-items-center">
<span class="p-1 bg-success rounded-circle me-2"></span> Tersedia
</span>
{% elif m.status == 'Stok Menipis' %}
<span class="badge bg-warning-subtle text-warning rounded-pill px-3 py-2 d-inline-flex align-items-center">
<span class="p-1 bg-warning rounded-circle me-2"></span> Menipis
</span>
{% else %}
<span class="badge bg-danger-subtle text-danger rounded-pill px-3 py-2 d-inline-flex align-items-center">
<span class="p-1 bg-danger rounded-circle me-2"></span> Habis
</span>
{% endif %}
</td>
<td class="text-end pe-3">
<div class="btn-group">
<a href="/admin/core/medicine/{{ m.pk }}/change/" class="btn btn-outline-primary btn-sm rounded-pill px-3 d-flex align-items-center" title="Edit Data">
<i data-lucide="edit-2" class="icon-sm me-1"></i> Edit
</a>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="8" class="text-center py-5">
<div class="text-muted">
<i data-lucide="search-x" class="mb-3 d-block mx-auto opacity-20" style="width: 64px; height: 64px;"></i>
<h5 class="fw-bold">Tidak ada data ditemukan</h5>
<p class="small mb-0">Coba ubah kata kunci pencarian atau filter Anda.</p>
<a href="{% url 'medicine_list' %}" class="btn btn-link btn-sm mt-2 text-decoration-none">Reset Semua Filter</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Add Medicine Modal -->
<div class="modal fade" id="addMedicineModal" tabindex="-1" aria-labelledby="addMedicineModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content glass-card shadow-lg border-0">
<div class="modal-header border-0 pb-0 ps-4 pt-4">
<h5 class="modal-title fw-bold d-flex align-items-center" id="addMedicineModalLabel">
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-2">
<i data-lucide="plus" class="icon-sm"></i>
</div>
Tambah Data Barang Baru
</h5>
<button type="button" class="btn-close me-2" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST">
{% csrf_token %}
<div class="modal-body p-4">
<p class="text-muted small mb-4">Lengkapi informasi berikut untuk menambahkan barang/obat ke dalam sistem.</p>
<div class="row g-4">
<div class="col-md-8">
<label class="form-label small fw-bold text-dark">Nama Barang/Obat <span class="text-danger">*</span></label>
{{ form.name }}
</div>
<div class="col-md-4">
<label class="form-label small fw-bold text-dark">SKU / Kode Unik <span class="text-danger">*</span></label>
{{ form.sku }}
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-dark">Kategori Barang <span class="text-danger">*</span></label>
{{ form.category }}
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-dark">Satuan <span class="text-danger">*</span></label>
{{ form.unit }}
</div>
<div class="col-md-4">
<label class="form-label small fw-bold text-dark">Stok Minimal (Alert) <span class="text-danger">*</span></label>
{{ form.min_stock }}
<div class="form-text smaller">Sistem akan memberi peringatan jika stok di bawah angka ini.</div>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold text-dark">Supplier Utama</label>
{{ form.main_supplier }}
</div>
<div class="col-md-4">
<label class="form-label small fw-bold text-dark">Supplier Alternatif</label>
{{ form.alternative_supplier }}
</div>
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-primary rounded-pill px-5 fw-bold shadow-sm">Simpan Data Barang</button>
</div>
</form>
</div>
</div>
</div>
<style>
.smaller { font-size: 0.75rem; }
.btn-white { background-color: white; border: 1px solid #dee2e6; }
.btn-transparent { background-color: transparent; border: 1px solid transparent; }
.table-hover tbody tr:hover { background-color: rgba(13, 110, 253, 0.02); }
</style>
{% endblock %}

View File

@ -0,0 +1,86 @@
{% extends 'base.html' %}
{% block content %}
<div class="row g-4">
<div class="col-md-4">
<div class="card glass-card border-0 shadow-sm">
<div class="card-body p-4">
<h4 class="fw-bold mb-4 d-flex align-items-center">
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-3">
<i data-lucide="user-plus"></i>
</div>
Tambah Supplier
</h4>
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
<label class="form-label small fw-bold text-muted text-uppercase mb-1" style="font-size: 0.65rem; letter-spacing: 0.5px;">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}
<div class="form-text small">{{ field.help_text }}</div>
{% endif %}
{% for error in field.errors %}
<div class="text-danger small">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<button type="submit" class="btn btn-primary w-100 py-3 mt-3 shadow-sm d-flex align-items-center justify-content-center">
<i data-lucide="save" class="me-2 icon-sm"></i> Simpan Supplier
</button>
</form>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card glass-card border-0 shadow-sm">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-bold mb-0 d-flex align-items-center">
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-3">
<i data-lucide="truck"></i>
</div>
Daftar Supplier
</h4>
<span class="badge bg-light text-dark border px-3 py-2 rounded-pill">{{ suppliers|length }} Total</span>
</div>
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Nama Supplier</th>
<th>Kontak</th>
<th>Telepon</th>
<th class="text-end">Aksi</th>
</tr>
</thead>
<tbody>
{% for s in suppliers %}
<tr>
<td>
<div class="fw-bold text-dark">{{ s.name }}</div>
<div class="text-muted small">{{ s.address|truncatechars:40 }}</div>
</td>
<td>{{ s.contact_person|default:"-" }}</td>
<td><span class="badge bg-light text-primary fw-medium">{{ s.phone|default:"-" }}</span></td>
<td class="text-end">
<a href="/admin/core/supplier/{{ s.id }}/change/" class="btn btn-light btn-sm rounded-circle p-2">
<i data-lucide="edit-3" class="icon-sm"></i>
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center py-5 text-muted">
<i data-lucide="inbox" class="mb-2 d-block mx-auto opacity-20" style="width: 48px; height: 48px;"></i>
Belum ada data supplier.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,69 @@
{% extends "base.html" %}
{% load static %}
{% block content %}
<div class="row justify-content-center align-items-center" style="min-height: 70vh;">
<div class="col-md-5 col-lg-4">
<div class="card glass-card border-0 shadow-lg p-2">
<div class="card-body p-4 p-md-5">
<div class="text-center mb-5">
<div class="bg-primary text-white rounded-4 d-inline-flex align-items-center justify-content-center mb-3" style="width: 64px; height: 64px; box-shadow: 0 10px 20px rgba(67, 97, 238, 0.3);">
<i data-lucide="shield-check" size="32"></i>
</div>
<h2 class="fw-bold text-dark">{{ project_name|default:"DN-WRS" }}</h2>
<p class="text-muted small">Masukkan kredensial Anda untuk mengakses portal</p>
</div>
{% if form.errors %}
<div class="alert alert-danger border-0 small py-3 rounded-4 d-flex align-items-center mb-4">
<i data-lucide="alert-circle" class="me-2" size="18"></i>
<span>Username atau password salah.</span>
</div>
{% endif %}
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<div class="mb-3">
<label for="id_username" class="form-label small fw-bold text-uppercase opacity-50">Username</label>
<div class="input-group">
<span class="input-group-text bg-light border-light rounded-start-3">
<i data-lucide="user" size="18" class="text-muted"></i>
</span>
<input type="text" name="username" autofocus maxlength="150" required id="id_username" class="form-control rounded-end-3 py-2 border-light bg-light">
</div>
</div>
<div class="mb-4">
<label for="id_password" class="form-label small fw-bold text-uppercase opacity-50">Password</label>
<div class="input-group">
<span class="input-group-text bg-light border-light rounded-start-3">
<i data-lucide="lock" size="18" class="text-muted"></i>
</span>
<input type="password" name="password" required id="id_password" class="form-control rounded-end-3 py-2 border-light bg-light">
</div>
</div>
<div class="d-grid pt-2">
<button type="submit" class="btn btn-primary py-3 rounded-3 d-flex align-items-center justify-content-center">
<span>Masuk</span>
<i data-lucide="arrow-right" class="ms-2" size="18"></i>
</button>
</div>
<input type="hidden" name="next" value="{{ next }}">
</form>
</div>
</div>
<div class="text-center mt-4 opacity-50">
<small>Diproteksi oleh DN-Security</small>
</div>
</div>
</div>
<style>
body {
background: radial-gradient(circle at 0% 0%, rgba(67, 97, 238, 0.05) 0%, transparent 50%),
radial-gradient(circle at 100% 100%, rgba(114, 9, 183, 0.05) 0%, transparent 50%),
linear-gradient(135deg, #f6f8fd 0%, #f1f4f9 100%);
}
</style>
{% endblock %}

View File

@ -1,7 +1,17 @@
from django.urls import path
from .views import home
from . import views
urlpatterns = [
path("", home, name="home"),
path('', views.home, name='home'),
path('medicines/', views.medicine_list, name='medicine_list'),
path('medicines/export-low-stock/', views.export_low_stock_pdf, name='export_low_stock_pdf'),
path('suppliers/', views.supplier_list, name='supplier_list'),
path('categories/', views.category_list, name='category_list'),
path('faktur/input/', views.input_faktur, name='input_faktur'),
path('faktur/<int:pk>/', views.faktur_detail, name='faktur_detail'),
path('barang-keluar/', views.barang_keluar, name='barang_keluar'),
path('api/batches/', views.get_batches, name='get_batches'),
path('laporan/', views.laporan_transaksi, name='laporan_transaksi'),
path('transaksi/<int:pk>/delete/', views.delete_transaksi, name='delete_transaksi'),
path('transaksi/<int:pk>/edit/', views.edit_transaksi, name='edit_transaksi'),
]

View File

@ -1,25 +1,403 @@
import os
import platform
import io
from datetime import timedelta
from django import get_version as django_version
from django.shortcuts import render
from django.shortcuts import render, redirect, get_object_or_404
from django.utils import timezone
from django.db.models import Sum, Q
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.http import JsonResponse, HttpResponse
from .models import Medicine, Batch, Category, StockTransaction, Supplier, Faktur
from .forms import SupplierForm, FakturForm, StockInForm, StockOutForm, CategoryForm, MedicineForm
from reportlab.lib import colors
from reportlab.lib.pagesizes import letter, A4
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
@login_required
def home(request):
"""Render the landing screen with loader and environment details."""
host_name = request.get_host().lower()
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
"""Render the medicine warehouse dashboard."""
now = timezone.now()
today = now.date()
# Stats
total_medicines = Medicine.objects.count()
# Total stock across all batches
total_stock = Batch.objects.aggregate(total=Sum('quantity'))['total'] or 0
# Expired batches
expired_batches_count = Batch.objects.filter(expiry_date__lte=today).count()
# Near expiry (next 90 days)
near_expiry_batches_count = Batch.objects.filter(
expiry_date__gt=today,
expiry_date__lte=today + timezone.timedelta(days=90)
).count()
# Low stock medicines
all_medicines = Medicine.objects.all()
low_stock_count = 0
for med in all_medicines:
if med.total_stock <= med.min_stock:
low_stock_count += 1
# Latest medicines for the table
recent_medicines = all_medicines.order_by('-created_at')[:5]
# Chart Data: Last 7 days movement
chart_labels = []
chart_data_in = []
chart_data_out = []
for i in range(6, -1, -1):
day = today - timedelta(days=i)
chart_labels.append(day.strftime('%d %b'))
in_sum = StockTransaction.objects.filter(
transaction_type='IN',
created_at__date=day
).aggregate(total=Sum('quantity'))['total'] or 0
out_sum = StockTransaction.objects.filter(
transaction_type='OUT',
created_at__date=day
).aggregate(total=Sum('quantity'))['total'] or 0
chart_data_in.append(in_sum)
chart_data_out.append(out_sum)
context = {
"project_name": "New Style",
"agent_brand": agent_brand,
"project_name": "DN-WRS",
"total_medicines": total_medicines,
"total_stock": total_stock,
"expired_count": expired_batches_count,
"near_expiry_count": near_expiry_batches_count,
"low_stock_count": low_stock_count,
"recent_medicines": recent_medicines,
"chart_labels": chart_labels,
"chart_data_in": chart_data_in,
"chart_data_out": chart_data_out,
"django_version": django_version(),
"python_version": platform.python_version(),
"current_time": now,
"host_name": host_name,
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
}
return render(request, "core/index.html", context)
# --- MASTER DATA MANAGEMENT ---
@login_required
def category_list(request):
if request.method == 'POST':
form = CategoryForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, "Kategori berhasil ditambahkan.")
return redirect('category_list')
else:
form = CategoryForm()
categories = Category.objects.all().order_by('name')
return render(request, 'core/categories.html', {
'categories': categories,
'form': form,
'project_name': 'DN-WRS'
})
@login_required
def supplier_list(request):
if request.method == 'POST':
form = SupplierForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, "Supplier berhasil ditambahkan.")
return redirect('supplier_list')
else:
form = SupplierForm()
suppliers = Supplier.objects.all().order_by('name')
return render(request, 'core/suppliers.html', {
'suppliers': suppliers,
'form': form,
'project_name': 'DN-WRS'
})
@login_required
def medicine_list(request):
query = request.GET.get('q')
show_low_stock = request.GET.get('low_stock') == '1'
if request.method == 'POST':
form = MedicineForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, "Data barang berhasil ditambahkan.")
return redirect('medicine_list')
else:
form = MedicineForm()
medicines_all = Medicine.objects.all().order_by('name')
# Calculate low stock count for the summary
low_stock_count = 0
for med in medicines_all:
if med.total_stock <= med.min_stock:
low_stock_count += 1
medicines = medicines_all
if query:
medicines = medicines.filter(
Q(name__icontains=query) |
Q(sku__icontains=query) |
Q(category__name__icontains=query)
)
if show_low_stock:
# Filter for low stock
low_stock_ids = [m.id for m in medicines if m.total_stock <= m.min_stock]
medicines = medicines.filter(id__in=low_stock_ids)
return render(request, 'core/medicines.html', {
'medicines': medicines,
'form': form,
'project_name': 'DN-WRS',
'query': query,
'show_low_stock': show_low_stock,
'low_stock_count': low_stock_count,
'total_count': medicines_all.count()
})
@login_required
def export_low_stock_pdf(request):
# Get low stock medicines
all_medicines = Medicine.objects.all().order_by('main_supplier__name', 'name')
low_stock_medicines = [m for m in all_medicines if m.total_stock <= m.min_stock]
# Group by main supplier
grouped = {}
for m in low_stock_medicines:
supplier_name = m.main_supplier.name if m.main_supplier else "Tanpa Supplier Utama"
if supplier_name not in grouped:
grouped[supplier_name] = []
grouped[supplier_name].append(m)
# Generate PDF
buffer = io.BytesIO()
doc = SimpleDocTemplate(buffer, pagesize=A4)
elements = []
styles = getSampleStyleSheet()
title_style = ParagraphStyle(
'Title',
parent=styles['Heading1'],
fontSize=18,
alignment=1,
spaceAfter=20,
textColor=colors.HexColor("#0d6efd")
)
elements.append(Paragraph("Laporan Daftar Stok Menipis", title_style))
elements.append(Paragraph(f"Tanggal Cetak: {timezone.now().strftime('%d %B %Y %H:%M')}", styles["Normal"]))
elements.append(Spacer(1, 20))
if not low_stock_medicines:
elements.append(Paragraph("Tidak ada stok yang menipis saat ini.", styles["Normal"]))
else:
for supplier, items in grouped.items():
elements.append(Paragraph(f"Supplier Utama: {supplier}", styles["Heading2"]))
elements.append(Spacer(1, 5))
data = [["Nama Barang", "SKU", "Stok", "Min.", "Satuan", "Supplier Alternatif"]]
for item in items:
alt_supplier = item.alternative_supplier.name if item.alternative_supplier else "-"
data.append([
item.name,
item.sku,
str(item.total_stock),
str(item.min_stock),
item.unit,
alt_supplier
])
table = Table(data, colWidths=[160, 60, 40, 40, 50, 110])
table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#343a40")),
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 10),
('BOTTOMPADDING', (0, 0), (-1, 0), 10),
('TOPPADDING', (0, 0), (-1, 0), 10),
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
('FONTSIZE', (0, 1), (-1, -1), 9),
]))
elements.append(table)
elements.append(Spacer(1, 15))
doc.build(elements)
buffer.seek(0)
response = HttpResponse(buffer, content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="laporan_stok_menipis.pdf"'
return response
# --- TRANSAKSI ---
@login_required
def input_faktur(request):
if request.method == 'POST':
faktur_form = FakturForm(request.POST)
if faktur_form.is_valid():
faktur = faktur_form.save()
return redirect('faktur_detail', pk=faktur.pk)
else:
faktur_form = FakturForm(initial={'faktur_type': 'MASUK', 'date': timezone.now().date()})
fakturs = Faktur.objects.all().order_by('-created_at')
return render(request, 'core/input_faktur.html', {
'faktur_form': faktur_form,
'fakturs': fakturs,
'project_name': 'DN-WRS'
})
@login_required
def faktur_detail(request, pk):
faktur = get_object_or_404(Faktur, pk=pk)
if request.method == 'POST':
form = StockInForm(request.POST)
if form.is_valid():
# Create Batch
batch = Batch.objects.create(
medicine=form.cleaned_data['medicine'],
faktur=faktur,
batch_number=form.cleaned_data['batch_number'],
expiry_date=form.cleaned_data['expiry_date'],
quantity=form.cleaned_data['quantity'],
buying_price=form.cleaned_data['buying_price'],
selling_price=form.cleaned_data['selling_price']
)
# Create Transaction
StockTransaction.objects.create(
medicine=batch.medicine,
batch=batch,
faktur=faktur,
transaction_type='IN',
quantity=batch.quantity,
note=f"Input dari Faktur {faktur.faktur_number}"
)
messages.success(request, f"Barang {batch.medicine.name} berhasil ditambahkan.")
return redirect('faktur_detail', pk=pk)
else:
form = StockInForm()
items = Batch.objects.filter(faktur=faktur)
return render(request, 'core/faktur_detail.html', {
'faktur': faktur,
'form': form,
'items': items,
'project_name': 'DN-WRS'
})
@login_required
def barang_keluar(request):
if request.method == 'POST':
form = StockOutForm(request.POST)
if form.is_valid():
medicine = form.cleaned_data['medicine']
batch = form.cleaned_data['batch']
qty = form.cleaned_data['quantity']
if batch.quantity < qty:
messages.error(request, f"Stok tidak mencukupi. Stok saat ini: {batch.quantity}")
else:
# Update Batch
batch.quantity -= qty
batch.save()
# Create Transaction
StockTransaction.objects.create(
medicine=medicine,
batch=batch,
transaction_type='OUT',
quantity=qty,
note=form.cleaned_data['note']
)
messages.success(request, f"Barang keluar berhasil dicatat.")
return redirect('barang_keluar')
else:
form = StockOutForm()
transactions = StockTransaction.objects.filter(transaction_type='OUT').order_by('-created_at')[:10]
return render(request, 'core/barang_keluar.html', {
'form': form,
'transactions': transactions,
'project_name': 'DN-WRS'
})
@login_required
def get_batches(request):
medicine_id = request.GET.get('medicine_id')
batches = Batch.objects.filter(medicine_id=medicine_id, quantity__gt=0).values('id', 'batch_number', 'quantity')
return JsonResponse(list(batches), safe=False)
@login_required
def laporan_transaksi(request):
transactions = StockTransaction.objects.all().order_by('-created_at')
return render(request, 'core/laporan_transaksi.html', {
'transactions': transactions,
'project_name': 'DN-WRS'
})
@login_required
def delete_transaksi(request, pk):
transaction = get_object_or_404(StockTransaction, pk=pk)
batch = transaction.batch
if batch:
if transaction.transaction_type == 'IN':
batch.quantity -= transaction.quantity
elif transaction.transaction_type == 'OUT':
batch.quantity += transaction.quantity
batch.save()
transaction.delete()
messages.success(request, "Transaksi berhasil dihapus dan stok telah diperbarui.")
return redirect('laporan_transaksi')
@login_required
def edit_transaksi(request, pk):
transaction = get_object_or_404(StockTransaction, pk=pk)
if request.method == 'POST':
new_qty = int(request.POST.get('quantity'))
old_qty = transaction.quantity
batch = transaction.batch
if batch:
if transaction.transaction_type == 'IN':
# Revert old, apply new
batch.quantity = batch.quantity - old_qty + new_qty
elif transaction.transaction_type == 'OUT':
# Revert old, apply new
batch.quantity = batch.quantity + old_qty - new_qty
if batch.quantity < 0:
messages.error(request, "Error: Stok tidak boleh negatif setelah perubahan.")
return redirect('laporan_transaksi')
batch.save()
transaction.quantity = new_qty
transaction.note = request.POST.get('note', transaction.note)
transaction.save()
messages.success(request, "Transaksi berhasil diperbarui.")
return redirect('laporan_transaksi')
return render(request, 'core/edit_transaksi.html', {
'transaction': transaction,
'project_name': 'DN-WRS'
})

View File

@ -1,9 +1,9 @@
<?php
// Generated by setup_mariadb_project.sh — edit as needed.
define('DB_HOST', '127.0.0.1');
define('DB_NAME', 'app_38226');
define('DB_USER', 'app_38226');
define('DB_PASS', 'c892d620-ef33-4130-848d-608166951927');
define('DB_NAME', 'app_38240');
define('DB_USER', 'app_38240');
define('DB_PASS', '0f262299-4701-4e42-af5e-1a1136825b7a');
function db() {
static $pdo;

View File

@ -1,3 +1,4 @@
Django==5.2.7
mysqlclient==2.2.7
python-dotenv==1.1.1
reportlab==4.1.0

View File

@ -1,4 +1,122 @@
/* Custom styles for the application */
body {
font-family: system-ui, -apple-system, sans-serif;
/* Modern UI Styles for DN-WRS */
:root {
--primary: #4361ee;
--primary-hover: #3a56d4;
--secondary: #7209b7;
--success: #4cc9f0;
--info: #4895ef;
--warning: #f72585;
--danger: #ef233c;
--dark: #2b2d42;
--light: #f8f9fa;
--glass: rgba(255, 255, 255, 0.7);
--glass-border: rgba(255, 255, 255, 0.3);
}
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background: linear-gradient(135deg, #f6f8fd 0%, #f1f4f9 100%);
background-attachment: fixed;
color: var(--dark);
min-height: 100vh;
}
/* Glassmorphism */
.glass-card {
background: var(--glass);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.07);
}
.card {
border: none;
border-radius: 1.25rem;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.05);
}
/* Navbar */
.navbar-custom {
background: rgba(255, 255, 255, 0.8) !important;
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--glass-border);
}
.navbar-brand {
font-size: 1.5rem;
letter-spacing: -0.5px;
}
/* Buttons */
.btn {
border-radius: 0.75rem;
padding: 0.6rem 1.5rem;
font-weight: 600;
transition: all 0.2s ease;
}
.btn-primary {
background: var(--primary);
border: none;
box-shadow: 0 4px 14px 0 rgba(67, 97, 238, 0.3);
}
.btn-primary:hover {
background: var(--primary-hover);
transform: scale(1.02);
box-shadow: 0 6px 20px rgba(67, 97, 238, 0.4);
}
/* Table */
.table {
border-collapse: separate;
border-spacing: 0 0.5rem;
}
.table tr {
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.02);
border-radius: 0.75rem;
}
.table td, .table th {
padding: 1rem;
vertical-align: middle;
}
.table thead th {
background: transparent;
border: none;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 1px;
color: #94a3b8;
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-in {
animation: fadeIn 0.6s ease-out forwards;
}
/* Stats Cards */
.stats-icon {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
margin-bottom: 1rem;
}