from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from enum import Enum
import uuid
import pickle
import os
from observer import Observable
# ==============================================================================
# 1. FINITE STATE MACHINE (FSM) IMPLEMENTATION
# ==============================================================================
class TransactionContext:
"""
Context untuk FSM Transaksi.
Objek ini akan mengubah perilakunya berdasarkan state saat ini.
"""
def __init__(self, member, item):
self.transaksi_id = "TRX-" + str(uuid.uuid4())[:8].upper()
self.member = member
self.item = item
self.tgl_pinjam = datetime.now()
self.tgl_kembali_rencana = self.tgl_pinjam + timedelta(days=14)
self.denda = 0.0
self._state = ActiveState()
def set_state(self, state):
self._state = state
def get_status_name(self):
return self._state.get_name()
def hitung_denda_saat_ini(self):
return self._state.calculate_fine(self)
def proses_pengembalian(self):
self._state.return_item(self)
def to_dict(self):
return {
"id": self.transaksi_id,
"item": self.item.judul,
"nama_member": self.member.nama,
"tgl_pinjam": self.tgl_pinjam.strftime('%Y-%m-%d'),
"tgl_kembali": self.tgl_kembali_rencana.strftime('%Y-%m-%d'),
"status": self.get_status_name(),
"denda": f"Rp{self.hitung_denda_saat_ini():,.2f}"
}
class TransactionState(ABC):
@abstractmethod
def get_name(self): pass
@abstractmethod
def calculate_fine(self, context): pass
@abstractmethod
def return_item(self, context): pass
class ActiveState(TransactionState):
def get_name(self): return "Aktif"
def calculate_fine(self, context):
if datetime.now() > context.tgl_kembali_rencana:
print(f"INFO FSM: Transisi State {context.transaksi_id} [Active -> Overdue]")
context.set_state(OverdueState())
return context.hitung_denda_saat_ini()
return 0.0
def return_item(self, context):
print(f"INFO FSM: Transisi State {context.transaksi_id} [Active -> Completed]")
context.item.ubah_status(StatusItem.TERSEDIA)
context.set_state(CompletedState())
class OverdueState(TransactionState):
def get_name(self): return "Terlambat"
def calculate_fine(self, context):
selisih_hari = (datetime.now() - context.tgl_kembali_rencana).days
return float(max(selisih_hari, 0) * 1000)
def return_item(self, context):
final_denda = self.calculate_fine(context)
context.denda = final_denda
print(f"INFO FSM: Transisi State {context.transaksi_id} [Overdue -> Completed] dengan denda {final_denda}")
context.item.ubah_status(StatusItem.TERSEDIA)
context.set_state(CompletedState())
class CompletedState(TransactionState):
def get_name(self): return "Selesai"
def calculate_fine(self, context): return context.denda
def return_item(self, context): raise Exception("Item sudah dikembalikan sebelumnya.")
class StatusItem(Enum):
TERSEDIA = "Tersedia"
DIPINJAM = "Dipinjam"
class LoanableItem(ABC):
def __init__(self, item_id, judul):
self.item_id = item_id
self.judul = judul
self.status = StatusItem.TERSEDIA
@abstractmethod
def get_details(self): pass
def ubah_status(self, status: StatusItem): self.status = status
class Buku(LoanableItem):
def __init__(self, item_id, judul, pengarang):
super().__init__(item_id, judul)
self.pengarang = pengarang
def get_details(self): return {"Tipe": "Buku", "Pengarang": self.pengarang}
class Majalah(LoanableItem):
def __init__(self, item_id, judul, edisi):
super().__init__(item_id, judul)
self.edisi = edisi
def get_details(self): return {"Tipe": "Majalah", "Edisi": self.edisi}
class ItemFactory:
@staticmethod
def create_item(tipe, item_id, judul, **kwargs):
if tipe.lower() == 'buku': return Buku(item_id, judul, kwargs.get('pengarang', '-'))
elif tipe.lower() == 'majalah': return Majalah(item_id, judul, kwargs.get('edisi', '-'))
raise ValueError("Tipe item tidak dikenal")
class Member:
def __init__(self, nama, alamat):
self.member_id = "MEM-" + str(uuid.uuid4())[:8].upper()
self.nama = nama
self.alamat = alamat
# --- OBSERVER PATTERN ---
class Perpustakaan(Observable): # Warisi Observable
def __init__(self, filepath="perpus_data.pkl"):
super().__init__() # Init Observable
self.filepath = filepath
self.items = []
self.members = []
self.transactions = []
self.load_data()
def add_item(self, item):
self.items.append(item)
# Notify View: Item bertambah
self.notify_observers(event="items_changed", data=self.items)
def add_member(self, member):
self.members.append(member)
# Notify View: Member bertambah
self.notify_observers(event="members_changed", data=self.members)
def create_transaction(self, member_id, item_id):
member = next((m for m in self.members if m.member_id == member_id), None)
item = next((i for i in self.items if i.item_id == item_id), None)
if not member or not item:
raise ValueError("Member atau Item tidak ditemukan")
if item.status != StatusItem.TERSEDIA:
raise ValueError("Item sedang dipinjam")
trx = TransactionContext(member, item)
self.transactions.append(trx)
item.ubah_status(StatusItem.DIPINJAM)
# Notify View: Transaksi baru & status item berubah
self.notify_observers(event="transactions_changed", data=self.transactions)
self.notify_observers(event="items_changed", data=self.items)
return trx
def return_transaction(self, trx_id):
trx = next((t for t in self.transactions if t.transaksi_id == trx_id), None)
if not trx:
raise ValueError("Transaksi tidak ditemukan")
trx.proses_pengembalian()
# Notify View: Transaksi update & status item berubah
self.notify_observers(event="transactions_changed", data=self.transactions)
self.notify_observers(event="items_changed", data=self.items)
return trx
def refresh_data(self):
"""Method manual untuk memicu notifikasi semua data (misal saat startup)."""
self.notify_observers(event="members_changed", data=self.members)
self.notify_observers(event="items_changed", data=self.items)
# Pastikan hitung denda terbaru sebelum notify
for t in self.transactions:
t.hitung_denda_saat_ini()
self.notify_observers(event="transactions_changed", data=self.transactions)
def save_data(self):
data = {"items": self.items, "members": self.members, "transactions": self.transactions}
with open(self.filepath, 'wb') as f: pickle.dump(data, f)
def load_data(self):
if os.path.exists(self.filepath):
try:
with open(self.filepath, 'rb') as f:
data = pickle.load(f)
self.items = data.get("items", [])
self.members = data.get("members", [])
self.transactions = data.get("transactions", [])
except Exception as e: print(f"Error loading data: {e}")