diff --git a/bootstrap.zip b/bootstrap.zip new file mode 100644 index 0000000..8d36c26 Binary files /dev/null and b/bootstrap.zip differ diff --git a/index.php b/index.php new file mode 100644 index 0000000..12bad28 --- /dev/null +++ b/index.php @@ -0,0 +1,5 @@ + diff --git a/routes/db_conn.php b/routes/db_conn.php new file mode 100644 index 0000000..76c78a1 --- /dev/null +++ b/routes/db_conn.php @@ -0,0 +1,16 @@ + diff --git a/src/img/logoayula.png b/src/img/logoayula.png new file mode 100644 index 0000000..723ccc1 Binary files /dev/null and b/src/img/logoayula.png differ diff --git a/src/img/smallest-ayula.png b/src/img/smallest-ayula.png new file mode 100644 index 0000000..3f651d3 Binary files /dev/null and b/src/img/smallest-ayula.png differ diff --git a/src/img/userprofile.png b/src/img/userprofile.png new file mode 100644 index 0000000..f46fcd3 Binary files /dev/null and b/src/img/userprofile.png differ diff --git a/src/img/warning.png b/src/img/warning.png new file mode 100644 index 0000000..b50a8d7 Binary files /dev/null and b/src/img/warning.png differ diff --git a/views/barang/addproduct.php b/views/barang/addproduct.php new file mode 100644 index 0000000..5a04982 --- /dev/null +++ b/views/barang/addproduct.php @@ -0,0 +1,385 @@ +query($query); + + if ($result->num_rows > 0) { + // Extract existing code + $row = $result->fetch_assoc(); + $lastCode = $row['kode_barang']; + + // Extract the numeric part and increment + $numericPart = intval(substr($lastCode, 3)); // Extract numbers after 'BRG' + $nextNumeric = $numericPart + 1; + + // Format with leading zeros (e.g., BRG001, BRG002, etc.) + $newCode = 'BRG' . str_pad($nextNumeric, 3, '0', STR_PAD_LEFT); + } else { + // If no existing codes, start with BRG001 + $newCode = 'BRG001'; + } + + return $newCode; +} + +if ($_SERVER["REQUEST_METHOD"] == "POST") { + // Generate new product code + $kode_barang = generateProductCode($conn); + + // Get data from form + $nama_barang = isset($_POST['nama_barang']) ? $_POST['nama_barang'] : ''; + $id_jenis = isset($_POST['id_jenis']) ? $_POST['id_jenis'] : ''; + $stok = isset($_POST['stok']) ? $_POST['stok'] : 0; + $harga = isset($_POST['harga']) ? $_POST['harga'] : ''; + + // Check if image file was uploaded + if (isset($_FILES['image']) && $_FILES['image']['error'] == 0) { + $image = $_FILES['image']; + + // Validate image + $image_name = basename($image['name']); + $target_dir = "../uploads/img-barang/"; // Target folder to store images + $target_file = $target_dir . $image_name; + $image_file_type = strtolower(pathinfo($target_file, PATHINFO_EXTENSION)); + + // Check if uploaded file is an image + if (in_array($image_file_type, ['jpg', 'jpeg', 'png', 'gif'])) { + // Move file to target folder + if (!move_uploaded_file($image['tmp_name'], $target_file)) { + echo "Error: Failed to upload image!"; + exit; + } + } else { + echo "Error: Only images (JPG, JPEG, PNG, GIF) are allowed!"; + exit; + } + } else { + $image_name = ''; // If no image was uploaded, set image name to empty + } + + // Validate id_jenis + $query_jenis = "SELECT id_jenis FROM jenis_barang WHERE id_jenis = ?"; + $stmt_jenis = $conn->prepare($query_jenis); + $stmt_jenis->bind_param("i", $id_jenis); + $stmt_jenis->execute(); + $stmt_jenis->store_result(); + + if ($stmt_jenis->num_rows == 0 && $id_jenis != '') { + echo "Error: Category not found!"; + exit; + } + + // Validate stock and price must be numbers + if (!preg_match('/^\d+$/', $stok) || !preg_match('/^\d+$/', $harga)) { + die(""); + } + + // Convert to integers for safety + $stok = intval($stok); + $harga = intval($harga); + + // Insert data into database (now including kode_barang) + $sql = "INSERT INTO barang (kode_barang, nama_barang, id_jenis, stok, harga, image) + VALUES (?, ?, ?, ?, ?, ?)"; + $stmt = $conn->prepare($sql); + $stmt->bind_param("ssisis", $kode_barang, $nama_barang, $id_jenis, $stok, $harga, $image_name); + + if ($stmt->execute()) { + // Redirect with success parameter + header("Location: productlist.php?success_add=1"); + exit(); +}} + +// Fetch categories for the dropdown +$query_categories = "SELECT id_jenis, nama_jenis FROM jenis_barang ORDER BY nama_jenis"; +$result_categories = $conn->query($query_categories); + +?> + + + + + + + + + + + + Dreams Pos admin template + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ + + + + + + + + + + + + +
+ + + +
+
+ +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ +
+
+ +
+ +
+ img +

Pilih Gambar

+
+
+
+
+
+ + Batal +
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/views/barang/create_upload_dir.php b/views/barang/create_upload_dir.php new file mode 100644 index 0000000..de4ec60 --- /dev/null +++ b/views/barang/create_upload_dir.php @@ -0,0 +1,49 @@ +Permissions set to 0755"; + + // Create .htaccess file to protect direct access to images (optional) + $htaccess_content = "# Deny direct access to files + + Order Allow,Deny + Deny from all + + +# Allow access through PHP scripts + + Order Allow,Deny + Allow from all +"; + + file_put_contents($upload_dir . '/.htaccess', $htaccess_content); + echo "
.htaccess file created for security"; + + } else { + echo "Failed to create directory: " . $upload_dir; + echo "
Please create this directory manually and ensure it has write permissions."; + } +} else { + echo "Directory already exists: " . $upload_dir; + + // Check if directory is writable + if (is_writable($upload_dir)) { + echo "
Directory is writable."; + } else { + echo "
Warning: Directory is not writable. Please set proper permissions."; + } +} + +echo "

Return to Product List"; +?> \ No newline at end of file diff --git a/views/barang/editproduct.php b/views/barang/editproduct.php new file mode 100644 index 0000000..1a03d25 --- /dev/null +++ b/views/barang/editproduct.php @@ -0,0 +1,404 @@ +connect_error) { + die("Connection failed: " . $conn->connect_error); + } + + return $conn; +} + +// Get product by ID +function getProductById($id) { + $conn = dbConnect(); + + $sql = "SELECT * FROM barang WHERE id_barang = ?"; + $stmt = $conn->prepare($sql); + $stmt->bind_param("i", $id); + $stmt->execute(); + $result = $stmt->get_result(); + $product = $result->fetch_assoc(); + + $stmt->close(); + $conn->close(); + + return $product; +} + +// Get all product categories +function getProductCategories() { + $conn = dbConnect(); + + $sql = "SELECT * FROM jenis_barang ORDER BY nama_jenis"; + $result = $conn->query($sql); + $categories = []; + + if ($result->num_rows > 0) { + while($row = $result->fetch_assoc()) { + $categories[] = $row; + } + } + + $conn->close(); + return $categories; +} +//fungsi update product +function updateProduct($id, $data) { + $conn = dbConnect(); + + $sql = "UPDATE barang SET + nama_barang = ?, + harga = ?, + stok = ?, + id_jenis = ?, + image = ? + WHERE id_barang = ?"; + + $stmt = $conn->prepare($sql); + // Change from "ssdiis" to "ssdisi" - notice the 'i' to 's' change for image parameter + $stmt->bind_param("ssdisi", + $data['nama_barang'], + $data['harga'], + $data['stok'], + $data['id_jenis'], + $data['image'], + $id + ); + + $result = $stmt->execute(); + + $stmt->close(); + $conn->close(); + + return $result; +} + +// Check if form is submitted +if ($_SERVER["REQUEST_METHOD"] == "POST") { + $id = $_POST['id_barang']; + + // Handle image upload if a new file is selected + $image = $_POST['current_image']; // Default to current image + + if(isset($_FILES['product_image']) && $_FILES['product_image']['size'] > 0) { + $target_dir = "../uploads/img-barang/"; + $file_extension = strtolower(pathinfo($_FILES["product_image"]["name"], PATHINFO_EXTENSION)); + $new_filename = "product_" . time() . "." . $file_extension; + $target_file = $target_dir . $new_filename; + + // Check if image file is an actual image + $check = getimagesize($_FILES["product_image"]["tmp_name"]); + if($check !== false) { + // Upload file + if (move_uploaded_file($_FILES["product_image"]["tmp_name"], $target_file)) { + $image = $new_filename; + } + } + } + + // Prepare data for update + $data = [ + 'nama_barang' => $_POST['nama_barang'], + 'harga' => $_POST['harga'], + 'stok' => $_POST['stok'], + 'id_jenis' => $_POST['id_jenis'], + 'image' => $image + ]; + + // Update product + if(updateProduct($id, $data)) { + // Redirect to product list with success message + // After successfully editing a product +header("Location: productlist.php?success_edit=1"); +exit(); + } else { + $error = "Failed to update product"; + } +} + +// Get product ID from URL +$product_id = isset($_GET['id']) ? $_GET['id'] : 0; +$product = getProductById($product_id); + +// Get all categories +$categories = getProductCategories(); + +// If product doesn't exist, redirect to product list +if (!$product) { + header("Location: productlist.php?error=Product not found"); + exit(); +} +?> + + + + + + + + + + + Ayula Store - Edit Product + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ + + + + + + + + + + + + +
+
+ + + +
+
+ + +
+
+
+ + + + +
+
+
+ + +
+
+
+
+ + +
+
+ +
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+ +
+ img +

Pilih Gambar

+
+
+
+
+ + +
+
+
    +
  • +
    +
    + img +
    +
    +
    +

    +
    +
    +
    +
  • +
+
+
+ + +
+ + Batal +
+
+
+
+
+
+
+ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/views/barang/export_report.php b/views/barang/export_report.php new file mode 100644 index 0000000..49d6b6b --- /dev/null +++ b/views/barang/export_report.php @@ -0,0 +1,98 @@ +connect_error) { + die("Koneksi gagal: " . $conn->connect_error); +} + +// Ambil data untuk laporan +$sql = "SELECT r.id_report, r.tanggal, b.nama_barang, jb.nama_jenis, r.jumlah, r.harga + FROM report r + JOIN barang b ON r.id_barang = b.id_barang + JOIN jenis_barang jb ON b.id_jenis = jb.id_jenis + ORDER BY r.tanggal DESC"; + +$result = $conn->query($sql); + +// Set header for Excel file +header('Content-Type: application/vnd.ms-excel'); +header('Content-Disposition: attachment; filename="laporan_barang_' . date('Y-m-d') . '.xls"'); +header('Pragma: no-cache'); +header('Expires: 0'); + +// Output tabel HTML yang akan dibaca sebagai Excel +echo ' + + + + Laporan Barang + + + +

Laporan Barang - Ayula Store

+

Tanggal Export: ' . date('d/m/Y H:i:s') . '

+ + + + + + + + + + + + + + + '; + +$no = 1; +$grand_total = 0; + +if ($result->num_rows > 0) { + while ($row = $result->fetch_assoc()) { + $total = $row['jumlah'] * $row['harga']; + $grand_total += $total; + + echo ' + + + + + + + + + '; + } + + echo ' + + + '; +} else { + echo ''; +} + +echo ' +
NoID LaporanTanggalNama BarangJenisJumlahHarga (Rp)Total (Rp)
' . $no++ . '' . $row['id_report'] . '' . date('d/m/Y H:i', strtotime($row['tanggal'])) . '' . $row['nama_barang'] . '' . $row['nama_jenis'] . '' . $row['jumlah'] . '' . number_format($row['harga'], 0, ',', '.') . '' . number_format($total, 0, ',', '.') . '
Grand Total' . number_format($grand_total, 0, ',', '.') . '
Tidak ada data laporan
+ +'; + +// Close database connection +$conn->close(); +?> \ No newline at end of file diff --git a/views/barang/image/Capture.PNG b/views/barang/image/Capture.PNG new file mode 100644 index 0000000..cc0f0e4 Binary files /dev/null and b/views/barang/image/Capture.PNG differ diff --git a/views/barang/image/mmm.JPEG b/views/barang/image/mmm.JPEG new file mode 100644 index 0000000..b2a8ce3 Binary files /dev/null and b/views/barang/image/mmm.JPEG differ diff --git a/views/barang/image/online-shopping.png b/views/barang/image/online-shopping.png new file mode 100644 index 0000000..ced34dd Binary files /dev/null and b/views/barang/image/online-shopping.png differ diff --git a/views/barang/image/product_1748803296.png b/views/barang/image/product_1748803296.png new file mode 100644 index 0000000..90a95d4 Binary files /dev/null and b/views/barang/image/product_1748803296.png differ diff --git a/views/barang/print_report.php b/views/barang/print_report.php new file mode 100644 index 0000000..0eb0418 --- /dev/null +++ b/views/barang/print_report.php @@ -0,0 +1,248 @@ +connect_error) { + die("Koneksi gagal: " . $conn->connect_error); +} + +// Cek ID laporan +if (!isset($_GET['id']) || empty($_GET['id'])) { + die("ID Laporan tidak valid"); +} + +$id_report = $_GET['id']; + +// Query untuk mengambil data laporan +$sql = "SELECT r.id_report, r.tanggal, b.id_barang, b.nama_barang, jb.nama_jenis, + r.jumlah, r.harga, r.image + FROM report r + JOIN barang b ON r.id_barang = b.id_barang + JOIN jenis_barang jb ON b.id_jenis = jb.id_jenis + WHERE r.id_report = ?"; + +$stmt = $conn->prepare($sql); +$stmt->bind_param("s", $id_report); +$stmt->execute(); +$result = $stmt->get_result(); + +if ($result->num_rows == 0) { + die("Laporan tidak ditemukan"); +} + +$report = $result->fetch_assoc(); +$total = $report['jumlah'] * $report['harga']; +$image_url = !empty($report['image']) ? '../uploads/nota/' . $report['image'] : '/ayula-store/bootstrap/assets/img/no-image.png'; +?> + + + + + + Cetak Laporan - <?php echo $report['id_report']; ?> + + + + +
+
+ + +
+ +
+
LAPORAN BARANG
+
AYULA STORE
+
+ +
+ Ayula Store
+ Jl. Contoh No. 123, Kota
+ Telp: (021) 123-4567
+ Email: info@ayulastore.com +
+ +
+ + + + + + + + + + + + + +
ID Laporan: Tanggal:
ID Barang: Jenis Barang:
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
NoNama BarangJumlahHargaTotal
1Rp Rp
TotalRp
+ +
+

Gambar Nota

+ Nota +
+ +
+
+
+ Penanggung Jawab +
+
+
+ Admin +
+
+
+ + + + + + \ No newline at end of file diff --git a/views/barang/product_details.php b/views/barang/product_details.php new file mode 100644 index 0000000..7b6b546 --- /dev/null +++ b/views/barang/product_details.php @@ -0,0 +1,349 @@ +prepare($sql); +$stmt->bind_param('i', $product_id); // Bind product_id as integer +$stmt->execute(); +$result = $stmt->get_result(); + +if ($result->num_rows > 0) { + $product = $result->fetch_assoc(); +} else { + echo "Product not found."; +} +?> + + + + + + + + + + + + Dreams Pos admin template + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ + + + + + + + + + + + + +
+ + + +
+
+ + +
+
+
+
+ + + +
+
    +
  • +

    barang

    +
    +
  • +
  • +

    kode barang

    +
    +
  • +
  • +

    Stok

    +
    +
  • +
  • +

    Harga

    +
    Rp
    +
  • + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/views/barang/productlist.php b/views/barang/productlist.php new file mode 100644 index 0000000..e59ddb0 --- /dev/null +++ b/views/barang/productlist.php @@ -0,0 +1,1284 @@ +prepare($sql); +if (!empty($search)) { + $searchTerm = "%$search%"; + $stmt->bind_param('ss', $searchTerm, $searchTerm); +} +$stmt->execute(); +$result = $stmt->get_result(); + +// Delete logic +if (isset($_GET['delete_id'])) { + $id = $_GET['delete_id']; + + // Disable foreign key checks + $conn->query("SET FOREIGN_KEY_CHECKS = 0"); + + // Check if the ID exists before attempting to delete + $checkSql = "SELECT id_barang FROM barang WHERE id_barang = ?"; + $checkStmt = $conn->prepare($checkSql); + $checkStmt->bind_param('i', $id); + $checkStmt->execute(); + $checkStmt->store_result(); + + if ($checkStmt->num_rows > 0) { + // Proceed with deletion if the item exists + $deleteSql = "DELETE FROM barang WHERE id_barang = ?"; + $deleteStmt = $conn->prepare($deleteSql); + $deleteStmt->bind_param('i', $id); + if ($deleteStmt->execute()) { + // After deletion, enable foreign key checks again + $conn->query("SET FOREIGN_KEY_CHECKS = 1"); + header("Location: productlist.php?success_delete=1"); + exit(); + } else { + // If deletion failed, re-enable foreign key checks + $conn->query("SET FOREIGN_KEY_CHECKS = 1"); + header("Location: productlist.php?success_delete=0"); + exit(); + } + } else { + // Item does not exist + $conn->query("SET FOREIGN_KEY_CHECKS = 1"); + header("Location: productlist.php?success_delete=0"); + exit(); + } +}?> + + + + + + + + + + + Dreams Pos admin template + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ + + + + + + + + + + + + +
+ + +
+
+ + +
+
+
+
+ +
+ +
+
+
+
    +
  • + +
  • + +
  • + +
  • + +
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ img +
+
+
+
+
+
+
+ + + +
+ + + + + + + + + + + + + + + + num_rows > 0) { + while ($row = $result->fetch_assoc()) { + echo " + + + + + + + + "; + } + } else { + echo ""; + } + ?> + + +
+ + Kode BarangNama BarangJenisStokHargaAksi
+ + {$row['kode_barang']}{$row['nama_barang']}{$row['nama_jenis']}{$row['stok']}Rp " . number_format($row['harga'], 0, ',', '.') . " + + Lihat Detail + + + Edit + + + Hapus + + + Pindah ke Kasir + +
Tidak ada data barang
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/views/barang/report_handler.php b/views/barang/report_handler.php new file mode 100644 index 0000000..71833bc --- /dev/null +++ b/views/barang/report_handler.php @@ -0,0 +1,122 @@ +connect_error) { + die(json_encode(['success' => false, 'message' => 'Koneksi database gagal: ' . $conn->connect_error])); +} + +// Cek action yang diminta +if (isset($_POST['action']) && $_POST['action'] == 'create_report') { + createReport($conn); +} else { + echo json_encode(['success' => false, 'message' => 'Aksi tidak valid']); +} + +function createReport($conn) { + // Handle upload gambar nota + $upload_dir = '../uploads/nota/'; + + // Buat direktori jika belum ada + if (!file_exists($upload_dir)) { + mkdir($upload_dir, 0777, true); + } + + $image_name = ''; + + if (isset($_FILES['receipt_image']) && $_FILES['receipt_image']['error'] == 0) { + // Generate nama file unik + $file_extension = pathinfo($_FILES['receipt_image']['name'], PATHINFO_EXTENSION); + $image_name = 'nota_' . time() . '_' . mt_rand(1000, 9999) . '.' . $file_extension; + $target_file = $upload_dir . $image_name; + + // Validasi tipe file + $allowed_types = ['jpg', 'jpeg', 'png', 'gif']; + if (!in_array(strtolower($file_extension), $allowed_types)) { + echo json_encode(['success' => false, 'message' => 'Format file tidak didukung. Gunakan JPG, JPEG, PNG, atau GIF']); + return; + } + + // Validasi ukuran file (max 5MB) + if ($_FILES['receipt_image']['size'] > 5 * 1024 * 1024) { + echo json_encode(['success' => false, 'message' => 'Ukuran file terlalu besar (maksimum 5MB)']); + return; + } + + // Upload file + if (!move_uploaded_file($_FILES['receipt_image']['tmp_name'], $target_file)) { + echo json_encode(['success' => false, 'message' => 'Gagal mengunggah gambar']); + return; + } + } else { + echo json_encode(['success' => false, 'message' => 'Gambar nota harus diunggah']); + return; + } + + // Mulai transaksi + $conn->begin_transaction(); + + try { + // Loop through the items to insert + if (isset($_POST['id_barang'])) { + foreach ($_POST['id_barang'] as $key => $id_barang) { + $jumlah = $_POST['jumlah'][$key] ?? ''; + $harga = $_POST['harga'][$key] ?? ''; + + // Validasi data + if (empty($id_barang) || empty($jumlah) || empty($harga)) { + echo json_encode(['success' => false, 'message' => 'Semua field harus diisi']); + return; + } + + // Buat ID report otomatis (format: RPT-YYYYMMDDxxx) + $date = date('Ymd'); + $query = "SELECT MAX(SUBSTRING(id_report, 12)) as last_id FROM report WHERE id_report LIKE 'RPT-$date%'"; + $result = $conn->query($query); + $row = $result->fetch_assoc(); + $last_id = $row['last_id'] ?? 0; + $new_id = 'RPT-' . $date . str_pad(intval($last_id) + 1, 3, '0', STR_PAD_LEFT); + + // Simpan data ke tabel report + $query = "INSERT INTO report (id_report, id_barang, tanggal, jumlah, harga, image) + VALUES (?, ?, NOW(), ?, ?, ?)"; + $stmt = $conn->prepare($query); + $stmt->bind_param('sssss', $new_id, $id_barang, $jumlah, $harga, $image_name); + $stmt->execute(); + + if ($stmt->affected_rows <= 0) { + throw new Exception("Gagal menyimpan data report"); + } + } + } else { + throw new Exception("ID Barang tidak ditemukan"); + } + + // Commit transaksi + $conn->commit(); + + echo json_encode([ + 'success' => true, + 'message' => 'Data berhasil ditambahkan ke laporan', + 'image_url' => '/ayula-store/uploads/nota/' . $image_name + ]); + + } catch (Exception $e) { + // Rollback transaksi jika terjadi kesalahan + $conn->rollback(); + + // Hapus file gambar jika upload sudah terjadi + if (!empty($image_name) && file_exists($upload_dir . $image_name)) { + unlink($upload_dir . $image_name); + } + + echo json_encode(['success' => false, 'message' => 'Error: ' . $e->getMessage()]); + } +} +?> diff --git a/views/barang/reportlist.php b/views/barang/reportlist.php new file mode 100644 index 0000000..49d6b6b --- /dev/null +++ b/views/barang/reportlist.php @@ -0,0 +1,98 @@ +connect_error) { + die("Koneksi gagal: " . $conn->connect_error); +} + +// Ambil data untuk laporan +$sql = "SELECT r.id_report, r.tanggal, b.nama_barang, jb.nama_jenis, r.jumlah, r.harga + FROM report r + JOIN barang b ON r.id_barang = b.id_barang + JOIN jenis_barang jb ON b.id_jenis = jb.id_jenis + ORDER BY r.tanggal DESC"; + +$result = $conn->query($sql); + +// Set header for Excel file +header('Content-Type: application/vnd.ms-excel'); +header('Content-Disposition: attachment; filename="laporan_barang_' . date('Y-m-d') . '.xls"'); +header('Pragma: no-cache'); +header('Expires: 0'); + +// Output tabel HTML yang akan dibaca sebagai Excel +echo ' + + + + Laporan Barang + + + +

Laporan Barang - Ayula Store

+

Tanggal Export: ' . date('d/m/Y H:i:s') . '

+ + + + + + + + + + + + + + + '; + +$no = 1; +$grand_total = 0; + +if ($result->num_rows > 0) { + while ($row = $result->fetch_assoc()) { + $total = $row['jumlah'] * $row['harga']; + $grand_total += $total; + + echo ' + + + + + + + + + '; + } + + echo ' + + + '; +} else { + echo ''; +} + +echo ' +
NoID LaporanTanggalNama BarangJenisJumlahHarga (Rp)Total (Rp)
' . $no++ . '' . $row['id_report'] . '' . date('d/m/Y H:i', strtotime($row['tanggal'])) . '' . $row['nama_barang'] . '' . $row['nama_jenis'] . '' . $row['jumlah'] . '' . number_format($row['harga'], 0, ',', '.') . '' . number_format($total, 0, ',', '.') . '
Grand Total' . number_format($grand_total, 0, ',', '.') . '
Tidak ada data laporan
+ +'; + +// Close database connection +$conn->close(); +?> \ No newline at end of file diff --git a/views/barang/tes.php b/views/barang/tes.php new file mode 100644 index 0000000..ef4c15f --- /dev/null +++ b/views/barang/tes.php @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/views/barang/topsis_functions.php b/views/barang/topsis_functions.php new file mode 100644 index 0000000..2e02c52 --- /dev/null +++ b/views/barang/topsis_functions.php @@ -0,0 +1,174 @@ + 0.4, + 'price_value' => 0.3, + 'turnover_rate' => 0.3, + ]; + } + + // Ambil data produk dan penjualan 30 hari terakhir sesuai struktur DB + $query = " + SELECT + bk.id_barangK AS id_barang, + bk.nama_barang, + bk.stok, + bk.harga, + COALESCE(SUM(dt.jumlah), 0) AS total_penjualan + FROM barang_kasir bk + LEFT JOIN detail_transaksi dt ON bk.id_barangK = dt.id_barangK + LEFT JOIN transaksi t ON dt.id_transaksi = t.id_transaksi + AND t.tanggal BETWEEN DATE_SUB(NOW(), INTERVAL 30 DAY) AND NOW() + GROUP BY bk.id_barangK, bk.nama_barang, bk.stok, bk.harga + "; + + $result = mysqli_query($conn, $query); + if (!$result) { + return ["error" => "Query failed: " . mysqli_error($conn)]; + } + + $products = []; + while ($row = mysqli_fetch_assoc($result)) { + // Pastikan harga sudah dalam format numeric (decimal) + $harga = (float)$row['harga']; + + // Hitung turnover rate (penjualan/stok), hindari pembagian 0 + $turnover_rate = ($row['stok'] > 0) ? ($row['total_penjualan'] / $row['stok']) : 0; + + $products[] = [ + 'id_barang' => $row['id_barang'], + 'nama_barang' => $row['nama_barang'], + 'stok' => (int)$row['stok'], + 'harga' => $harga, + 'turnover_rate' => $turnover_rate, + ]; + } + + if (empty($products)) { + return ["message" => "No products found"]; + } + + // Buat decision matrix dengan reciprocal stok (1/stok) + $decision_matrix = []; + foreach ($products as $product) { + $reciprocal_stock = ($product['stok'] > 0) ? (1 / $product['stok']) : 0; + $decision_matrix[] = [ + 'id_barang' => $product['id_barang'], + 'nama_barang' => $product['nama_barang'], + 'stock_level' => $reciprocal_stock, + 'price_value' => $product['harga'], + 'turnover_rate' => $product['turnover_rate'], + ]; + } + + // Normalisasi + $normalized_matrix = normalizeMatrix($decision_matrix); + + // Terapkan bobot + $weighted_matrix = applyWeights($normalized_matrix, $criteria_weights); + + // Tentukan solusi ideal positif dan negatif + $ideal_solution = []; + $negative_ideal_solution = []; + foreach (['stock_level', 'price_value', 'turnover_rate'] as $criteria) { + $values = array_column($weighted_matrix, $criteria); + $ideal_solution[$criteria] = max($values); + $negative_ideal_solution[$criteria] = min($values); + } + + // Hitung jarak ke solusi ideal positif dan negatif + $separation_ideal = []; + $separation_negative = []; + foreach ($weighted_matrix as $key => $product) { + $separation_ideal[$key] = sqrt( + pow($product['stock_level'] - $ideal_solution['stock_level'], 2) + + pow($product['price_value'] - $ideal_solution['price_value'], 2) + + pow($product['turnover_rate'] - $ideal_solution['turnover_rate'], 2) + ); + $separation_negative[$key] = sqrt( + pow($product['stock_level'] - $negative_ideal_solution['stock_level'], 2) + + pow($product['price_value'] - $negative_ideal_solution['price_value'], 2) + + pow($product['turnover_rate'] - $negative_ideal_solution['turnover_rate'], 2) + ); + } + + // Hitung skor TOPSIS (relative closeness) + $relative_closeness = []; + foreach ($weighted_matrix as $key => $product) { + $denominator = $separation_negative[$key] + $separation_ideal[$key]; + $relative_closeness[$key] = ($denominator > 0) ? ($separation_negative[$key] / $denominator) : 0; + } + + // Siapkan hasil + $topsis_scores = []; + foreach ($relative_closeness as $key => $score) { + $topsis_scores[] = [ + 'id_barang' => $decision_matrix[$key]['id_barang'], + 'nama_barang' => $decision_matrix[$key]['nama_barang'], + 'stok' => $products[$key]['stok'], + 'harga' => $products[$key]['harga'], + 'topsis_score' => round($score, 4), + 'rank' => 0, + ]; + } + + // Urutkan berdasarkan skor TOPSIS descending + usort($topsis_scores, function($a, $b) { + return $b['topsis_score'] <=> $a['topsis_score']; + }); + + // Beri peringkat + foreach ($topsis_scores as $key => $product) { + $topsis_scores[$key]['rank'] = $key + 1; + } + + return $topsis_scores; +} + +function normalizeMatrix($matrix) { + $normalized = []; + $sum_squares = [ + 'stock_level' => 0, + 'price_value' => 0, + 'turnover_rate' => 0, + ]; + + foreach ($matrix as $product) { + $sum_squares['stock_level'] += pow($product['stock_level'], 2); + $sum_squares['price_value'] += pow($product['price_value'], 2); + $sum_squares['turnover_rate'] += pow($product['turnover_rate'], 2); + } + + foreach ($sum_squares as $key => $value) { + $sum_squares[$key] = sqrt($value); + } + + foreach ($matrix as $key => $product) { + $normalized[$key] = [ + 'id_barang' => $product['id_barang'], + 'nama_barang' => $product['nama_barang'], + 'stock_level' => ($sum_squares['stock_level'] > 0) ? ($product['stock_level'] / $sum_squares['stock_level']) : 0, + 'price_value' => ($sum_squares['price_value'] > 0) ? ($product['price_value'] / $sum_squares['price_value']) : 0, + 'turnover_rate' => ($sum_squares['turnover_rate'] > 0) ? ($product['turnover_rate'] / $sum_squares['turnover_rate']) : 0, + ]; + } + + return $normalized; +} + +function applyWeights($matrix, $weights) { + $weighted = []; + foreach ($matrix as $key => $product) { + $weighted[$key] = [ + 'id_barang' => $product['id_barang'], + 'nama_barang' => $product['nama_barang'], + 'stock_level' => $product['stock_level'] * $weights['stock_level'], + 'price_value' => $product['price_value'] * $weights['price_value'], + 'turnover_rate' => $product['turnover_rate'] * $weights['turnover_rate'], + ]; + } + return $weighted; +} +?> diff --git a/views/barang/topsis_implementation.php b/views/barang/topsis_implementation.php new file mode 100644 index 0000000..a0e52ef --- /dev/null +++ b/views/barang/topsis_implementation.php @@ -0,0 +1,141 @@ + false, + 'message' => 'Invalid product ID or amount' + ]; + } + + // Ambil stok sekarang dari barang_kasir + $query = "SELECT stok FROM barang_kasir WHERE id_barangK = ?"; + $stmt = mysqli_prepare($conn, $query); + mysqli_stmt_bind_param($stmt, "i", $productId); + mysqli_stmt_execute($stmt); + $result = mysqli_stmt_get_result($stmt); + + if (!$row = mysqli_fetch_assoc($result)) { + return [ + 'success' => false, + 'message' => 'Product not found' + ]; + } + + $currentStock = (int)$row['stok']; + $newStock = $currentStock + $amount; + + // Update stok barang_kasir + $updateQuery = "UPDATE barang_kasir SET stok = ? WHERE id_barangK = ?"; + $updateStmt = mysqli_prepare($conn, $updateQuery); + mysqli_stmt_bind_param($updateStmt, "ii", $newStock, $productId); + $success = mysqli_stmt_execute($updateStmt); + + if (!$success) { + return [ + 'success' => false, + 'message' => 'Failed to update stock: ' . mysqli_error($conn) + ]; + } + + // Log aktivitas restok + $logQuery = "INSERT INTO log_restok (id_barang, jumlah, tanggal, catatan) + VALUES (?, ?, NOW(), ?)"; + $logStmt = mysqli_prepare($conn, $logQuery); + mysqli_stmt_bind_param($logStmt, "iis", $productId, $amount, $note); + mysqli_stmt_execute($logStmt); + + return [ + 'success' => true, + 'message' => 'Stock updated successfully', + 'new_stock' => $newStock + ]; +} + +// Handle AJAX request +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) { + $host = "localhost"; + $username = "root"; // sesuaikan username DB Anda + $password = ""; // sesuaikan password DB Anda + $database = "ayula_store"; + + $conn = mysqli_connect($host, $username, $password, $database); + if (!$conn) { + echo json_encode([ + 'success' => false, + 'message' => 'Database connection failed: ' . mysqli_connect_error() + ]); + exit; + } + + $action = $_POST['action']; + + switch ($action) { + case 'restock': + if (isset($_POST['productId']) && isset($_POST['amount'])) { + $result = processRestock( + $conn, + $_POST['productId'], + $_POST['amount'], + $_POST['note'] ?? '' + ); + echo json_encode($result); + } else { + echo json_encode([ + 'success' => false, + 'message' => 'Missing required parameters' + ]); + } + break; + default: + echo json_encode([ + 'success' => false, + 'message' => 'Unknown action' + ]); + } + + mysqli_close($conn); + exit; +} + +function createRestockLogTable($conn) { + $query = "CREATE TABLE IF NOT EXISTS log_restok ( + id INT(11) PRIMARY KEY AUTO_INCREMENT, + id_barang INT(11) NOT NULL, + jumlah INT(10) NOT NULL, + tanggal DATETIME NOT NULL, + catatan TEXT, + FOREIGN KEY (id_barang) REFERENCES barang_kasir(id_barangK) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"; + + return mysqli_query($conn, $query); +} + +function exportToCSV($data, $filename = 'topsis_results.csv') { + header('Content-Type: text/csv'); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + + $output = fopen('php://output', 'w'); + + fputcsv($output, ['Rank', 'ID Barang', 'Nama Barang', 'Stok', 'Harga', 'TOPSIS Score']); + + foreach ($data as $row) { + fputcsv($output, [ + $row['rank'], + $row['id_barang'], + $row['nama_barang'], + $row['stok'], + $row['harga'], + $row['topsis_score'] + ]); + } + + fclose($output); + exit; +} +?> diff --git a/views/barang/topsis_restock_view.php b/views/barang/topsis_restock_view.php new file mode 100644 index 0000000..665486e --- /dev/null +++ b/views/barang/topsis_restock_view.php @@ -0,0 +1,627 @@ + 0.4, // Lower stock is more urgent (40% importance) + 'price_value' => 0.3, // Higher price might indicate higher priority (30% importance) + 'turnover_rate' => 0.3 // Higher turnover rate means faster selling (30% importance) +]; + +// Check if form was submitted with custom weights +$weights = $default_weights; +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['submit_weights'])) { + $weights = [ + 'stock_level' => floatval($_POST['weight_stock']), + 'price_value' => floatval($_POST['weight_price']), + 'turnover_rate' => floatval($_POST['weight_turnover']) + ]; + + // Normalize weights to ensure they sum to 1 + $total = array_sum($weights); + if ($total > 0) { + foreach ($weights as $key => $value) { + $weights[$key] = $value / $total; + } + } else { + $weights = $default_weights; + } +} + +// Get restocking priorities using TOPSIS +$restock_priorities = restockWithTOPSIS($conn, $weights); + +// Check if any error occurred +$error_message = ""; +if (isset($restock_priorities['error'])) { + $error_message = $restock_priorities['error']; + $restock_priorities = []; +} + +// Calculate total products and those needing restocking (stock < 10) +$total_products = count($restock_priorities); +$low_stock_count = 0; +foreach ($restock_priorities as $product) { + if ($product['stok'] < 10) { + $low_stock_count++; + } +} + +// Get current date and time +$current_date = date('Y-m-d H:i:s'); + +// Page title +$page_title = "Analisis Restok dengan Metode TOPSIS"; +?> + + + + + + + + + + + Dreams Pos admin template + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ + + + + + + + + + + + + +
+ +
+

+ +
+
+
+
+
Total Produk
+

+
+
+
+
+
+
+
Produk Stok Rendah
+

+ Stok < 10 +
+
+
+
+
+
+
Tanggal Analisis
+

+
+
+
+
+ +
+
+
+

Kustomisasi Bobot Kriteria

+
+
+ + + Nilai lebih tinggi = stok rendah lebih penting +
+
+ + + Nilai lebih tinggi = harga tinggi lebih penting +
+
+ + + Nilai lebih tinggi = penjualan cepat lebih penting +
+
+ + +
+
+
+
+
+
+
+ Distribusi Bobot +
+
+ +
+
+
+
+ + + + +
+
+

Hasil Analisis TOPSIS

+
+
+
+ + + + + + + + + + + + + + 0.7) { + $priority_class = 'priority-high'; + } elseif ($product['topsis_score'] > 0.4) { + $priority_class = 'priority-medium'; + } else { + $priority_class = 'priority-low'; + } + ?> + + + + + + + + + + + +
PrioritasID BarangNama BarangStok Saat IniHargaAksi
Rp + +
+
+
+
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/views/barang/transfer.php b/views/barang/transfer.php new file mode 100644 index 0000000..1555f12 --- /dev/null +++ b/views/barang/transfer.php @@ -0,0 +1,334 @@ +connect_error) { + log_debug("Connection failed: " . $conn->connect_error); + echo json_encode([ + 'success' => false, + 'message' => 'Koneksi database gagal: ' . $conn->connect_error + ]); + exit; +} + +log_debug("Database connected successfully"); + +// Check if we're handling a single product or bulk transfer +$is_bulk = isset($_POST['product_ids']) && is_array($_POST['product_ids']); + +if ($is_bulk) { + // Bulk transfer + log_debug("Processing bulk transfer"); + + $product_ids = $_POST['product_ids']; + $quantities = $_POST['quantities']; + + // Validate input + if (empty($product_ids) || empty($quantities) || count($product_ids) != count($quantities)) { + log_debug("Invalid bulk data"); + echo json_encode([ + 'success' => false, + 'message' => 'Data tidak valid untuk transfer massal.' + ]); + exit; + } + + // Begin transaction + $conn->begin_transaction(); + + $success_count = 0; + $error_messages = []; + + for ($i = 0; $i < count($product_ids); $i++) { + $id_barang = $product_ids[$i]; + $quantity = intval($quantities[$i]); + + log_debug("Processing item $i: ID=$id_barang, Quantity=$quantity"); + + // Skip invalid items + if (empty($id_barang) || $quantity <= 0) { + $error_messages[] = "Data tidak valid untuk produk #$i"; + continue; + } + + // Get product details + $stmt = $conn->prepare("SELECT * FROM barang WHERE id_barang = ?"); + $stmt->bind_param("s", $id_barang); + $stmt->execute(); + $result = $stmt->get_result(); + + if ($result->num_rows == 0) { + $error_messages[] = "Produk dengan ID $id_barang tidak ditemukan"; + $stmt->close(); + continue; + } + + $product = $result->fetch_assoc(); + + // Check stock + if ($quantity > $product['stok']) { + $error_messages[] = "Stok tidak mencukupi untuk produk {$product['nama_barang']}"; + $stmt->close(); + continue; + } + + // Update stock in barang + $new_stock = $product['stok'] - $quantity; + $update_stmt = $conn->prepare("UPDATE barang SET stok = ? WHERE id_barang = ?"); + $update_stmt->bind_param("is", $new_stock, $id_barang); + + if (!$update_stmt->execute()) { + $error_messages[] = "Gagal memperbarui stok untuk produk {$product['nama_barang']}"; + $stmt->close(); + $update_stmt->close(); + continue; + } + + $update_stmt->close(); + + // Check if exists in barang_kasir + $check_stmt = $conn->prepare("SELECT * FROM barang_kasir WHERE kode_barang = ?"); + $check_stmt->bind_param("s", $product['kode_barang']); + $check_stmt->execute(); + $check_result = $check_stmt->get_result(); + + $timestamp = date('Y-m-d H:i:s'); + + if ($check_result->num_rows > 0) { + // Update existing record + $kasir_product = $check_result->fetch_assoc(); + $new_kasir_stock = $kasir_product['stok'] + $quantity; + + $kasir_update_sql = "UPDATE barang_kasir SET stok = ?, update_at = ? WHERE id_barangK = ?"; + $kasir_update_stmt = $conn->prepare($kasir_update_sql); + $kasir_update_stmt->bind_param("isi", $new_kasir_stock, $timestamp, $kasir_product['id_barangK']); + + if (!$kasir_update_stmt->execute()) { + $error_messages[] = "Gagal memperbarui stok di kasir untuk produk {$product['nama_barang']}"; + $stmt->close(); + $check_stmt->close(); + $kasir_update_stmt->close(); + continue; + } + + $kasir_update_stmt->close(); + } else { + // Insert new record + // Using direct query to avoid parameter binding issues + $kode_barang = $conn->real_escape_string($product['kode_barang']); + $nama_barang = $conn->real_escape_string($product['nama_barang']); + $id_jenis = (int)$product['id_jenis']; + $harga = $conn->real_escape_string($product['harga']); + $stok = (int)$quantity; + $gambar = $conn->real_escape_string($product['image'] ?? ''); + $timestamp_esc = $conn->real_escape_string($timestamp); + + $insert_query = "INSERT INTO barang_kasir (kode_barang, nama_barang, id_jenis, harga, stok, gambar, created_at, update_at) + VALUES ('$kode_barang', '$nama_barang', $id_jenis, '$harga', $stok, '$gambar', '$timestamp_esc', '$timestamp_esc')"; + + if (!$conn->query($insert_query)) { + $error_messages[] = "Gagal menambahkan produk {$product['nama_barang']} ke kasir"; + $stmt->close(); + $check_stmt->close(); + continue; + } + } + + $check_stmt->close(); + $stmt->close(); + $success_count++; + } + + if ($success_count == 0) { + // If no products were successfully processed + $conn->rollback(); + log_debug("Bulk transfer failed: " . implode(', ', $error_messages)); + echo json_encode([ + 'success' => false, + 'message' => 'Tidak ada produk yang berhasil dipindahkan: ' . implode(', ', $error_messages) + ]); + } else { + // Commit if at least one product was processed + $conn->commit(); + + if (count($error_messages) > 0) { + log_debug("Partial bulk transfer success: $success_count items, with errors: " . implode(', ', $error_messages)); + echo json_encode([ + 'success' => true, + 'message' => "Berhasil memindahkan $success_count produk ke kasir. Beberapa produk gagal: " . implode(', ', $error_messages) + ]); + } else { + log_debug("Complete bulk transfer success: $success_count items"); + echo json_encode([ + 'success' => true, + 'message' => "Berhasil memindahkan $success_count produk ke kasir." + ]); + } + } +} else { + // Single product transfer + if (!isset($_POST['id_barang']) || !isset($_POST['quantity'])) { + log_debug("Missing required parameters for single transfer"); + echo json_encode([ + 'success' => false, + 'message' => 'Parameter tidak lengkap' + ]); + exit; + } + + $id_barang = $_POST['id_barang']; + $quantity = (int)$_POST['quantity']; + + log_debug("Processing single transfer for product ID: $id_barang, quantity: $quantity"); + + // Validate data + if (empty($id_barang) || $quantity <= 0) { + log_debug("Invalid parameters"); + echo json_encode([ + 'success' => false, + 'message' => 'Parameter tidak valid' + ]); + exit; + } + + // Get product information + $stmt = $conn->prepare("SELECT * FROM barang WHERE id_barang = ?"); + $stmt->bind_param("s", $id_barang); + $stmt->execute(); + $result = $stmt->get_result(); + + if ($result->num_rows == 0) { + log_debug("Product not found"); + echo json_encode([ + 'success' => false, + 'message' => 'Barang tidak ditemukan' + ]); + $stmt->close(); + exit; + } + + $product = $result->fetch_assoc(); + log_debug("Product found: " . json_encode($product)); + + // Check if enough stock + if ($quantity > $product['stok']) { + log_debug("Insufficient stock"); + echo json_encode([ + 'success' => false, + 'message' => 'Stok tidak mencukupi' + ]); + $stmt->close(); + exit; + } + + // Begin transaction + $conn->begin_transaction(); + + try { + // 1. Update stock in barang + $new_stock = $product['stok'] - $quantity; + $update_stmt = $conn->prepare("UPDATE barang SET stok = ? WHERE id_barang = ?"); + $update_stmt->bind_param("is", $new_stock, $id_barang); + + if (!$update_stmt->execute()) { + throw new Exception("Failed to update product stock: " . $update_stmt->error); + } + + log_debug("Updated barang stock to: $new_stock"); + $update_stmt->close(); + + // 2. Check if product exists in barang_kasir + $check_stmt = $conn->prepare("SELECT * FROM barang_kasir WHERE kode_barang = ?"); + $check_stmt->bind_param("s", $product['kode_barang']); + $check_stmt->execute(); + $check_result = $check_stmt->get_result(); + + $timestamp = date('Y-m-d H:i:s'); + + if ($check_result->num_rows > 0) { + // 3a. Update existing product + $kasir_product = $check_result->fetch_assoc(); + $new_kasir_stock = $kasir_product['stok'] + $quantity; + + log_debug("Product exists in cashier, updating stock to: $new_kasir_stock"); + + $kasir_update_stmt = $conn->prepare("UPDATE barang_kasir SET stok = ?, update_at = ? WHERE id_barangK = ?"); + $kasir_update_stmt->bind_param("isi", $new_kasir_stock, $timestamp, $kasir_product['id_barangK']); + + if (!$kasir_update_stmt->execute()) { + throw new Exception("Failed to update cashier stock: " . $kasir_update_stmt->error); + } + + $kasir_update_stmt->close(); + } else { + // 3b. Insert new product with direct query + log_debug("Product doesn't exist in cashier, inserting new record"); + + // Set default empty string for image + $kode_barang = $conn->real_escape_string($product['kode_barang']); + $nama_barang = $conn->real_escape_string($product['nama_barang']); + $id_jenis = (int)$product['id_jenis']; + $harga = $conn->real_escape_string($product['harga']); + $stok = (int)$quantity; + $gambar = $conn->real_escape_string($product['image'] ?? ''); + $timestamp_esc = $conn->real_escape_string($timestamp); + + $insert_query = "INSERT INTO barang_kasir (kode_barang, nama_barang, id_jenis, harga, stok, gambar, created_at, update_at) + VALUES ('$kode_barang', '$nama_barang', $id_jenis, '$harga', $stok, '$gambar', '$timestamp_esc', '$timestamp_esc')"; + + log_debug("Insert query: $insert_query"); + + if (!$conn->query($insert_query)) { + throw new Exception("Failed to insert product into cashier: " . $conn->error); + } + } + + $check_stmt->close(); + + // Commit transaction + $conn->commit(); + + log_debug("Transfer completed successfully"); + + echo json_encode([ + 'success' => true, + 'message' => 'Berhasil memindahkan ' . $quantity . ' item ke kasir' + ]); + + } catch (Exception $e) { + // Rollback on error + $conn->rollback(); + + log_debug("Error occurred: " . $e->getMessage()); + + echo json_encode([ + 'success' => false, + 'message' => 'Terjadi kesalahan: ' . $e->getMessage() + ]); + } + + $stmt->close(); +} + +$conn->close(); +log_debug('Script completed'); +?> \ No newline at end of file diff --git a/views/barang/transfer_debug.log b/views/barang/transfer_debug.log new file mode 100644 index 0000000..8f6717e --- /dev/null +++ b/views/barang/transfer_debug.log @@ -0,0 +1,52 @@ +[2025-06-01 09:32:55] Script started +[2025-06-01 09:32:55] POST data: {"id_barang":"120","quantity":"1"} +[2025-06-01 09:32:55] Database connected successfully +[2025-06-01 09:32:55] Processing single transfer for product ID: 120, quantity: 1 +[2025-06-01 09:32:55] Product found: {"id_barang":120,"kode_barang":"BRG039","nama_barang":"Produk A","stok":5,"harga":"150000","id_jenis":8,"image":"kios-Halaman-2.drawio (2).png"} +[2025-06-01 09:32:55] Updated barang stock to: 4 +[2025-06-01 09:32:55] Product exists in cashier, updating stock to: 6 +[2025-06-01 09:32:55] Transfer completed successfully +[2025-06-01 09:32:55] Script completed +[2025-06-01 09:33:23] Script started +[2025-06-01 09:33:23] POST data: {"product_ids":["120","121"],"quantities":["1","1"]} +[2025-06-01 09:33:23] Database connected successfully +[2025-06-01 09:33:23] Processing bulk transfer +[2025-06-01 09:33:23] Processing item 0: ID=120, Quantity=1 +[2025-06-01 09:33:23] Processing item 1: ID=121, Quantity=1 +[2025-06-01 09:33:23] Complete bulk transfer success: 2 items +[2025-06-01 09:33:23] Script completed +[2025-06-01 09:33:33] Script started +[2025-06-01 09:33:33] POST data: {"id_barang":"122","quantity":"1"} +[2025-06-01 09:33:33] Database connected successfully +[2025-06-01 09:33:33] Processing single transfer for product ID: 122, quantity: 1 +[2025-06-01 09:33:33] Product found: {"id_barang":122,"kode_barang":"BRG041","nama_barang":"Produk B","stok":12,"harga":"75000","id_jenis":1,"image":"kios-Halaman-3.drawio (2).png"} +[2025-06-01 09:33:33] Updated barang stock to: 11 +[2025-06-01 09:33:33] Product exists in cashier, updating stock to: 13 +[2025-06-01 09:33:33] Transfer completed successfully +[2025-06-01 09:33:33] Script completed +[2025-06-01 21:07:57] Script started +[2025-06-01 21:07:57] POST data: {"id_barang":"120","quantity":"1"} +[2025-06-01 21:07:57] Database connected successfully +[2025-06-01 21:07:57] Processing single transfer for product ID: 120, quantity: 1 +[2025-06-01 21:07:57] Product found: {"id_barang":120,"kode_barang":"BRG039","nama_barang":"Produk A","stok":3,"harga":"150000","id_jenis":8,"image":"product_1748803380.png"} +[2025-06-01 21:07:57] Updated barang stock to: 2 +[2025-06-01 21:07:57] Product exists in cashier, updating stock to: 8 +[2025-06-01 21:07:57] Transfer completed successfully +[2025-06-01 21:07:57] Script completed +[2025-06-01 21:08:07] Script started +[2025-06-01 21:08:07] POST data: {"product_ids":["122","124"],"quantities":["1","1"]} +[2025-06-01 21:08:07] Database connected successfully +[2025-06-01 21:08:07] Processing bulk transfer +[2025-06-01 21:08:07] Processing item 0: ID=122, Quantity=1 +[2025-06-01 21:08:07] Processing item 1: ID=124, Quantity=1 +[2025-06-01 21:08:07] Complete bulk transfer success: 2 items +[2025-06-01 21:08:07] Script completed +[2025-06-03 03:21:32] Script started +[2025-06-03 03:21:32] POST data: {"id_barang":"122","quantity":"1"} +[2025-06-03 03:21:32] Database connected successfully +[2025-06-03 03:21:32] Processing single transfer for product ID: 122, quantity: 1 +[2025-06-03 03:21:32] Product found: {"id_barang":122,"kode_barang":"BRG041","nama_barang":"Produk B","stok":10,"harga":"75000","id_jenis":1,"image":"kios-Halaman-3.drawio (2).png"} +[2025-06-03 03:21:32] Updated barang stock to: 9 +[2025-06-03 03:21:32] Product exists in cashier, updating stock to: 15 +[2025-06-03 03:21:32] Transfer completed successfully +[2025-06-03 03:21:32] Script completed diff --git a/views/barang/transfer_handler.php b/views/barang/transfer_handler.php new file mode 100644 index 0000000..8a6ab6b --- /dev/null +++ b/views/barang/transfer_handler.php @@ -0,0 +1,216 @@ +connect_error) { + die(json_encode([ + 'success' => false, + 'message' => "Koneksi gagal: " . $conn->connect_error + ])); +} + +// Fungsi untuk memvalidasi data yang diterima +function validateData($data) { + $data = trim($data); + $data = stripslashes($data); + $data = htmlspecialchars($data); + return $data; +} + +// Handle single product transfer +if (isset($_POST['action']) && $_POST['action'] == 'transfer_product') { + // Ambil data dari POST + $id_barang = validateData($_POST['id_barang']); + $quantity = intval($_POST['quantity']); + + // Validasi input + if (empty($id_barang) || $quantity <= 0) { + echo json_encode([ + 'success' => false, + 'message' => 'Data tidak valid.' + ]); + exit; + } + + // Check if stock is sufficient + $sql = "SELECT stok FROM barang WHERE id_barang = ?"; + $stmt = $conn->prepare($sql); + $stmt->bind_param("s", $id_barang); + $stmt->execute(); + $result = $stmt->get_result(); + + if ($result->num_rows == 0) { + echo json_encode([ + 'success' => false, + 'message' => 'Barang tidak ditemukan.' + ]); + $stmt->close(); + exit; + } + + $row = $result->fetch_assoc(); + $current_stock = $row['stok']; + + if ($quantity > $current_stock) { + echo json_encode([ + 'success' => false, + 'message' => 'Jumlah melebihi stok tersedia.' + ]); + $stmt->close(); + exit; + } + + // Begin transaction + $conn->begin_transaction(); + + try { + // Update stock in barang table + $new_stock = $current_stock - $quantity; + $update_sql = "UPDATE barang SET stok = ? WHERE id_barang = ?"; + $update_stmt = $conn->prepare($update_sql); + $update_stmt->bind_param("is", $new_stock, $id_barang); + $update_stmt->execute(); + + // TODO: Insert data to cashier table or perform other actions as needed + // This will depend on your specific requirements for transferring to cashier + + // Commit the transaction + $conn->commit(); + + echo json_encode([ + 'success' => true, + 'message' => 'Berhasil memindahkan ' . $quantity . ' item ke kasir.' + ]); + + } catch (Exception $e) { + // Rollback transaction on error + $conn->rollback(); + + echo json_encode([ + 'success' => false, + 'message' => 'Terjadi kesalahan: ' . $e->getMessage() + ]); + } + + $stmt->close(); + if (isset($update_stmt)) { + $update_stmt->close(); + } +} + +// Handle bulk product transfers +if (isset($_POST['action']) && $_POST['action'] == 'bulk_transfer_products') { + // Get product IDs and quantities + $product_ids = isset($_POST['product_ids']) ? $_POST['product_ids'] : []; + $quantities = isset($_POST['quantities']) ? $_POST['quantities'] : []; + + // Validate input + if (empty($product_ids) || empty($quantities) || count($product_ids) != count($quantities)) { + echo json_encode([ + 'success' => false, + 'message' => 'Data tidak valid.' + ]); + exit; + } + + // Begin transaction + $conn->begin_transaction(); + + try { + $success_count = 0; + $error_messages = []; + + // Process each product + for ($i = 0; $i < count($product_ids); $i++) { + $id_barang = validateData($product_ids[$i]); + $quantity = intval($quantities[$i]); + + if (empty($id_barang) || $quantity <= 0) { + $error_messages[] = "Data tidak valid untuk produk #$i"; + continue; + } + + // Check if stock is sufficient + $sql = "SELECT stok FROM barang WHERE id_barang = ?"; + $stmt = $conn->prepare($sql); + $stmt->bind_param("s", $id_barang); + $stmt->execute(); + $result = $stmt->get_result(); + + if ($result->num_rows == 0) { + $error_messages[] = "Barang dengan ID $id_barang tidak ditemukan"; + $stmt->close(); + continue; + } + + $row = $result->fetch_assoc(); + $current_stock = $row['stok']; + + if ($quantity > $current_stock) { + $error_messages[] = "Jumlah melebihi stok tersedia untuk produk ID $id_barang"; + $stmt->close(); + continue; + } + + // Update stock in barang table + $new_stock = $current_stock - $quantity; + $update_sql = "UPDATE barang SET stok = ? WHERE id_barang = ?"; + $update_stmt = $conn->prepare($update_sql); + $update_stmt->bind_param("is", $new_stock, $id_barang); + $update_stmt->execute(); + $update_stmt->close(); + $stmt->close(); + + // TODO: Insert data to cashier table or perform other actions as needed + // This will depend on your specific requirements for transferring to cashier + + $success_count++; + } + + if ($success_count == 0) { + // If no products were successfully processed, rollback and return error + $conn->rollback(); + echo json_encode([ + 'success' => false, + 'message' => 'Tidak ada produk yang berhasil dipindahkan. ' . implode('; ', $error_messages) + ]); + } else { + // Commit the transaction if at least one product was successfully processed + $conn->commit(); + + if (count($error_messages) > 0) { + echo json_encode([ + 'success' => true, + 'message' => "Berhasil memindahkan $success_count produk ke kasir. Beberapa produk gagal: " . implode('; ', $error_messages) + ]); + } else { + echo json_encode([ + 'success' => true, + 'message' => "Berhasil memindahkan $success_count produk ke kasir." + ]); + } + } + + } catch (Exception $e) { + // Rollback transaction on error + $conn->rollback(); + + echo json_encode([ + 'success' => false, + 'message' => 'Terjadi kesalahan: ' . $e->getMessage() + ]); + } +} + +// Close connection +$conn->close(); +?> \ No newline at end of file diff --git a/views/barang/updateproduct.php b/views/barang/updateproduct.php new file mode 100644 index 0000000..d9fb12e --- /dev/null +++ b/views/barang/updateproduct.php @@ -0,0 +1,60 @@ + diff --git a/views/dashboard/index.php b/views/dashboard/index.php new file mode 100644 index 0000000..d430b96 --- /dev/null +++ b/views/dashboard/index.php @@ -0,0 +1,703 @@ +query($query); + $data['today_transactions'] = ($result->num_rows > 0) ? $result->fetch_assoc()['count'] : 0; + + // Today's items sold + $query = "SELECT SUM(total_item) as count FROM transaksi WHERE DATE(tanggal) = '$today'"; + $result = $conn->query($query); + $data['today_items'] = ($result->num_rows > 0) ? $result->fetch_assoc()['count'] : 0; + if ($data['today_items'] === NULL) $data['today_items'] = 0; + + // This week's transactions + $startOfWeek = date('Y-m-d', strtotime('this week monday')); + $query = "SELECT COUNT(*) as count FROM transaksi WHERE tanggal BETWEEN '$startOfWeek' AND '$today'"; + $result = $conn->query($query); + $data['week_transactions'] = ($result->num_rows > 0) ? $result->fetch_assoc()['count'] : 0; + + // Total products in inventory + $query = "SELECT COUNT(*) as count FROM barang"; + $result = $conn->query($query); + $data['total_products'] = ($result->num_rows > 0) ? $result->fetch_assoc()['count'] : 0; + + // Count low stock items (stok <= 10) + $query = "SELECT COUNT(*) as count FROM barang WHERE stok <= 10"; + $result = $conn->query($query); + $data['low_stock_count'] = ($result->num_rows > 0) ? $result->fetch_assoc()['count'] : 0; + + return $data; +} + +// Function to get recent transactions +function getRecentTransactions($conn, $limit = 5) { + $query = "SELECT t.kode_transaksi, t.tanggal, t.total_item, t.status + FROM transaksi t + ORDER BY t.tanggal DESC + LIMIT $limit"; + + $result = $conn->query($query); + $transactions = array(); + + if ($result->num_rows > 0) { + while($row = $result->fetch_assoc()) { + $transactions[] = $row; + } + } + + return $transactions; +} + +// Function to get popular products - FIXED to ensure it gets the most recent data +function getPopularProducts($conn, $limit = 10) { + // Use current date to ensure we get the latest data + $today = date('Y-m-d'); + $startOfWeek = date('Y-m-d', strtotime('this week monday')); + + // Modified query to join with detail_transaksi directly on id_transaksi + $query = "SELECT b.kode_barang, b.nama_barang, SUM(dt.jumlah) as total_quantity + FROM detail_transaksi dt + JOIN transaksi t ON dt.id_transaksi = t.id_transaksi + JOIN barang b ON dt.id_barang = b.id_barang + WHERE t.tanggal BETWEEN '$startOfWeek' AND NOW() + GROUP BY b.id_barang + ORDER BY total_quantity DESC + LIMIT $limit"; + + $result = $conn->query($query); + $products = array(); + + if ($result && $result->num_rows > 0) { + while($row = $result->fetch_assoc()) { + $products[] = $row; + } + } + + return $products; +} + +// Function to get low stock items +function getLowStockItems($conn, $threshold = 10, $limit = 5) { + $query = "SELECT kode_barang, nama_barang, stok + FROM barang + WHERE stok <= $threshold + ORDER BY stok ASC + LIMIT $limit"; + + $result = $conn->query($query); + $lowStockItems = array(); + + if ($result->num_rows > 0) { + while($row = $result->fetch_assoc()) { + $lowStockItems[] = $row; + } + } + + return $lowStockItems; +} + +// Get dashboard data +$dashboardData = getEmployeeDashboardData($conn); +$recentTransactions = getRecentTransactions($conn); +$popularProducts = getPopularProducts($conn); +$lowStockItems = getLowStockItems($conn); + +// Get user activity count (simplified) +$userActivityQuery = "SELECT COUNT(*) as count FROM transaksi WHERE tanggal > DATE_SUB(NOW(), INTERVAL 30 DAY)"; +$userActivityResult = $conn->query($userActivityQuery); +$userActivity = ($userActivityResult->num_rows > 0) ? $userActivityResult->fetch_assoc()['count'] : 0; + +// Recent products +$recentProductsQuery = "SELECT kode_barang, nama_barang, harga FROM barang ORDER BY created_at DESC LIMIT 5"; +$recentProductsResult = $conn->query($recentProductsQuery); +$recentProducts = array(); +if ($recentProductsResult->num_rows > 0) { + while($row = $recentProductsResult->fetch_assoc()) { + $recentProducts[] = $row; + } +} + +// Close database connection +$conn->close(); +?> + + + + + + + + + + + Ayula Store - Dashboard + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ + + + + + + + + + + + + +
+ + + +
+
+ +
+
+
+

Selamat Datang, !

+

Ringkasan aktivitas toko hari ini,

+
+
+ +
+
+
+ + +
+
+
+
+

+
Transaksi Hari Ini
+

+ +

+
+
+ +
+
+
+
+
+
+

+
Item Terjual Hari Ini
+

+ Total unit terjual +

+
+
+ +
+
+
+
+
+
+

+
Transaksi Minggu Ini
+

+ +

+
+
+ +
+
+
+
+
+
+

+
Produk Stok Menipis
+

+ Perlu perhatian +

+
+
+ +
+
+
+
+ + +
+ +
+
+
+
Transaksi Terbaru
+ Lihat Semua +
+
+ 0): ?> +
+ +
+
+
+

item

+
+
+ +

+
+
+ +
+ +
+ +
Belum ada transaksi hari ini
+

Transaksi baru akan muncul di sini

+
+ +
+
+
+ + +
+
+
+
Stok Menipis
+ Lihat Semua +
+
+ 0): ?> +
+ + + + + + + + + + + + + + + + + + + +
KodeNama ProdukStokStatus
+ + Kritis + + Menipis + +
+
+ +
+ +
Semua Produk Memiliki Stok Memadai
+

Tidak ada produk yang stoknya menipis saat ini

+
+ +
+
+
+
+ +
+ +
+
+
+
10 Produk Terlaris Minggu Ini
+ + Detail Lengkap + +
+
+ 0): ?> +
+ + + + +
+
+
+
+
+

+
+ # +
+
+
+ Total Terjual: + unit +
+
+
+
+
+
+
+
+ + + + 0): ?> + +
+
+
+
+
+

+
+ # +
+
+
+ Total Terjual: + unit +
+
+
+
+
+
+
+
+ + +
+ +
+ +
Belum Ada Data Penjualan
+

Produk terlaris akan ditampilkan saat ada penjualan

+
+ +
+
+
+
+
+
+
+ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/views/forgot-password.php b/views/forgot-password.php new file mode 100644 index 0000000..1c7244a --- /dev/null +++ b/views/forgot-password.php @@ -0,0 +1,118 @@ + 0) { + // Query untuk memperbarui password di database + $query = "UPDATE kasir SET password = '$hashed_password' WHERE username = '$username'"; + + if (mysqli_query($conn, $query)) { + // Jika berhasil, alihkan ke halaman login + $_SESSION['username'] = $username; + header("Location: index.php"); + exit(); + } else { + $error_message = "Gagal memperbarui password!"; + } + } else { + $error_message = "Username tidak ditemukan!"; + } + } +} +?> + + + + + + + + + + + + Reset Password - Ayula Store + + + + + + + + +
+ +
+ + + + + + + diff --git a/views/index.php b/views/index.php new file mode 100644 index 0000000..df366e1 --- /dev/null +++ b/views/index.php @@ -0,0 +1,278 @@ + 0) { + $user = mysqli_fetch_assoc($result); + + // Verifikasi password menggunakan password_verify() + if (password_verify($password, $user['password'])) { + // Jika password valid, login berhasil + $_SESSION['user_id'] = $user['id_kasir']; + $_SESSION['username'] = $user['username']; + $_SESSION['role'] = $user['role']; + + // Jika role adalah admin, tampilkan modal pilihan + if ($user['role'] == 'admin') { + // Set flag untuk menampilkan modal + $showRoleModal = true; + } else { + // Jika bukan admin, redirect langsung ke dashboard + header("Location: /ayula-store/views/dashboard/"); + exit(); + } + } else { + // Jika password salah + $error_message = "Password salah!"; + } + } else { + // Jika username tidak ditemukan + $error_message = "Username tidak ditemukan!"; + } +} + +// Proses pilihan role dari modal +if (isset($_POST['role_choice'])) { + if ($_POST['role_choice'] == 'gudang') { + header("Location: /ayula-store/views/reporttt/report.php"); + exit(); + } else if ($_POST['role_choice'] == 'kasir') { + header("Location: /ayula-store/views/dashboard/"); + exit(); + } +} + +// Proses logout jika tombol close ditekan +if (isset($_POST['logout'])) { + // Hapus semua data session + session_unset(); + session_destroy(); + // Redirect ke halaman login + header("Location: index.php"); + exit(); +} +?> + + + + + + + + + + + Login - Ayula Store + + + + + + + + +
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/views/logout.php b/views/logout.php new file mode 100644 index 0000000..f8f15d7 --- /dev/null +++ b/views/logout.php @@ -0,0 +1,24 @@ + \ No newline at end of file diff --git a/views/register.php b/views/register.php new file mode 100644 index 0000000..4eac3d5 --- /dev/null +++ b/views/register.php @@ -0,0 +1,110 @@ + 0) { + $error_message = "Username sudah terdaftar!"; + } else { + // Query untuk memasukkan data baru ke dalam database + $query = "INSERT INTO kasir (username, password, phone, role) VALUES ('$username', '$hashed_password', '$phone', 'user')"; + + if (mysqli_query($conn, $query)) { + // Jika berhasil, alihkan ke halaman login + $_SESSION['username'] = $username; + header("Location: index.php"); + exit(); + } else { + $error_message = "Gagal melakukan registrasi!"; + } + } +} +?> + + + + + + + + + + + Register - Ayula Store + + + + + + + +
+ +
+ + + + + + diff --git a/views/report-issue/index.php b/views/report-issue/index.php new file mode 100644 index 0000000..9b72589 --- /dev/null +++ b/views/report-issue/index.php @@ -0,0 +1,384 @@ + + + + + + + + + + + + + Ayula Store POS - Laporkan Masalah + + + + + + + + + + + + + +
+
+
+ +
+
+ + + + + + + + + + + + + +
+ + + + +
+
+ + +
+
+
Hubungi Developer via WhatsApp
+
+
+
+ + Isi formulir di bawah ini untuk melaporkan masalah. Pesan akan dikirim langsung ke developer melalui WhatsApp. +
+ +
+
+ +
+
+ + Bug/Error +
+
+ + Fitur Tidak Berfungsi +
+
+ + Permintaan Fitur +
+
+ + Optimasi Kinerja +
+
+ + UI/UX +
+
+ + Lainnya +
+
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+
+
+
+
+ + + + + + + + + + + + diff --git a/views/report/popular-products/index.php b/views/report/popular-products/index.php new file mode 100644 index 0000000..684d9c4 --- /dev/null +++ b/views/report/popular-products/index.php @@ -0,0 +1,949 @@ + strtotime($endDate)) { + $temp = $startDate; + $startDate = $endDate; + $endDate = $temp; +} + +// Default limit for top products +$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 20; +if ($limit < 5) $limit = 5; +if ($limit > 100) $limit = 100; + +// Default sort type (quantity or revenue) +$sortBy = isset($_GET['sort_by']) ? $_GET['sort_by'] : 'quantity'; +if (!in_array($sortBy, ['quantity', 'revenue'])) { + $sortBy = 'quantity'; +} + +// Get the popular products based on user role +$queryData = getPopularProducts($startDate, $endDate, $limit, $sortBy, $isAdmin); +$result = $queryData['result']; +$summary = $queryData['summary']; +$categories = $queryData['categories']; + +// Handle PDF export request if admin +if (isset($_POST['export_pdf']) || (isset($_GET['export']) && $_GET['export'] == 'pdf')) { + exportToPDF($startDate, $endDate, $limit, $sortBy, $isAdmin); +} +?> + + + + + + + + + + + + Ayula Store POS - Produk Terlaris + + + + + + + + + + + + + + + + + + + + + + +
+
+
Memuat data produk terlaris...
+
+ +
+
+
+ + + +
+
+ + + + + + + + + + + + + +
+ + + +
+
+ + + +
+
+
+
+ +
+
+
Total
+
Produk Terjual
+
+
+
+
+
+
+ +
+
+
Total
+
Item Terjual
+
+
+
+ +
+
+
+ +
+
+
Rp.
+
Total Pendapatan
+
+
+
+ + +
+
+
+ +
+
+
Laporan Aktivitas
+
Hubungi admin untuk detail keuangan
+
+
+
+ +
+ + + +
+
+
Filter Tanggal
+
+
+
+
+
+
+ + +
+
+ + + +
+
> +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+ +
+
+
+
+ + +
+ Rentang Tanggal: sampai + + + + +
+ +
+
+ + + 0): ?> +
+
+
+
+
Kategori Produk Terlaris
+
+
+
+ +
+
+
+
+
+
+
+
Distribusi Penjualan per Kategori
+
+
+
+ +
+
+
+
+
+ + + +
+
+
+ +
+
+ + +
+
+
+ + +
+
+ +
+

Tidak ada data penjualan produk untuk periode yang dipilih

+

+ + Tidak ada transaksi penjualan yang tercatat untuk tahun lalu (). + + Coba pilih rentang tanggal atau preset tanggal yang berbeda. + +

+ + Reset Filter + +
+ + +
+
+ +
+
+
+ +
+
+ +
+
+

Kode:

+

+
+
+ Terjual: + unit +
+ +
+ Harga: + Rp. +
+
+ Total: + Rp. +
+ + + +
+
+
+
+ + % dari total penjualan + +
+
+
+
+
+ +
+
+ + + + +
+
+ + + +
+
+
+
+
Rangkuman untuk Periode Terpilih
+
-
+
+
+
+
+
+
+ Total Produk Terjual: + produk +
+
+ Total Unit Terjual: + unit +
+
+
+
+ Rata-rata Per Hari: + 0 ? $summary['total_items_sold'] / $daysDiff : 0; + echo number_format($dailyAvgUnits, 1); + ?> unit/hari +
+
+ Pendapatan Rata-rata Per Hari: + Rp. 0 ? $summary['total_revenue'] / $daysDiff : 0; + echo number_format($dailyAvgRevenue); + ?> +
+
+
+
+ Total Pendapatan: + Rp. +
+
+ Rata-rata Pendapatan Per Produk: + Rp. 0 ? $summary['total_revenue'] / $summary['total_products'] : 0; + echo number_format($avgRevenuePerProduct); + ?> +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
Ringkasan Aktivitas Anda
+
+
+
+ + Untuk laporan keuangan terperinci atau untuk mengekspor data, silakan hubungi administrator Anda. +
+ +
+
+
+
+ Produk Dilihat: + produk teratas +
+
+ Total Unit Terjual (Diproses): + unit +
+
+
+
+ Laporan Dibuat: + +
+
+ Level Akses: + Karyawan +
+
+
+
+
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/views/report/popular-products/popular-products-config.php b/views/report/popular-products/popular-products-config.php new file mode 100644 index 0000000..7977b6d --- /dev/null +++ b/views/report/popular-products/popular-products-config.php @@ -0,0 +1,421 @@ + [ + 'label' => 'Hari Ini', + 'start' => date('Y-m-d'), + 'end' => date('Y-m-d') + ], + 'yesterday' => [ + 'label' => 'Kemarin', + 'start' => date('Y-m-d', strtotime('-1 day')), + 'end' => date('Y-m-d', strtotime('-1 day')) + ], + 'this_week' => [ + 'label' => 'Minggu Ini', + 'start' => date('Y-m-d', strtotime('monday this week')), + 'end' => date('Y-m-d') + ], + 'last_week' => [ + 'label' => 'Minggu Lalu', + 'start' => date('Y-m-d', strtotime('monday last week')), + 'end' => date('Y-m-d', strtotime('sunday last week')) + ], + 'this_month' => [ + 'label' => 'Bulan Ini', + 'start' => date('Y-m-01'), + 'end' => date('Y-m-d') + ], + ]; + + // Admin-only presets + if ($isAdmin) { + $presets['last_month'] = [ + 'label' => 'Bulan Lalu', + 'start' => date('Y-m-d', strtotime('first day of last month')), + 'end' => date('Y-m-d', strtotime('last day of last month')) + ]; + + $presets['last_90_days'] = [ + 'label' => '90 Hari Terakhir', + 'start' => date('Y-m-d', strtotime('-90 days')), + 'end' => date('Y-m-d') + ]; + + $presets['this_year'] = [ + 'label' => 'Tahun Ini', + 'start' => date('Y') . '-01-01', + 'end' => date('Y-m-d') + ]; + + $presets['all_time'] = [ + 'label' => 'Sepanjang Waktu', + 'start' => '2000-01-01', // Set a reasonable start date + 'end' => date('Y-m-d') + ]; + } + + return $presets; +} + +// Function to check if an employee has permission for a specific report feature +function canAccessFeature($feature) { + // Default permissions based on role + $permissions = [ + 'admin' => [ + 'export_excel' => true, + 'export_pdf' => true, + 'print_report' => true, + 'view_financial' => true, + 'view_payment_methods' => true, + 'view_all_time' => true, + 'view_summary' => true, + 'view_popular_products' => true + ], + 'manager' => [ + 'export_excel' => true, + 'export_pdf' => false, + 'print_report' => true, + 'view_financial' => true, + 'view_payment_methods' => true, + 'view_all_time' => false, + 'view_summary' => true, + 'view_popular_products' => true + ], + 'employee' => [ + 'export_excel' => false, + 'export_pdf' => false, + 'print_report' => false, + 'view_financial' => false, + 'view_payment_methods' => false, + 'view_all_time' => false, + 'view_summary' => false, + 'view_popular_products' => false + ], + 'user' => [ + 'export_excel' => false, + 'export_pdf' => false, + 'print_report' => false, + 'view_financial' => false, + 'view_payment_methods' => false, + 'view_all_time' => false, + 'view_summary' => false, + 'view_popular_products' => false + ] + ]; + + $role = getUserRole(); + + // If role doesn't exist in our permissions, default to employee + if (!isset($permissions[$role])) { + $role = 'employee'; + } + + // Return permission status + return isset($permissions[$role][$feature]) ? $permissions[$role][$feature] : false; +} + +// Function to get popular products +function getPopularProducts($startDate, $endDate, $limit = 20, $sortBy = 'quantity', $isAdmin = false) { + global $conn; + + // Add time components to make the date range inclusive + $startDateTime = $startDate . ' 00:00:00'; + $endDateTime = $endDate . ' 23:59:59'; + + // Base query for popular products - modified to join jenis_barang for category + $baseQuery = " + SELECT + b.id_barang, + b.kode_barang, + b.nama_barang, + b.harga, + j.nama_jenis as kategori, + SUM(dt.jumlah) as total_quantity, + SUM(dt.total_harga) as total_revenue + FROM + barang b + JOIN + jenis_barang j ON b.id_jenis = j.id_jenis + JOIN + detail_transaksi dt ON b.id_barang = dt.id_barang + JOIN + transaksi t ON dt.id_transaksi = t.id_transaksi + WHERE + t.tanggal BETWEEN ? AND ? + GROUP BY + b.id_barang + ORDER BY + "; + + // Add the appropriate ORDER BY clause + if ($sortBy == 'revenue') { + $baseQuery .= "total_revenue DESC"; + } else { + $baseQuery .= "total_quantity DESC"; + } + + // Add limit + $baseQuery .= " LIMIT ?"; + + // Prepare and execute query + $stmt = mysqli_prepare($conn, $baseQuery); + + if (!$stmt) { + return [ + 'result' => false, + 'error' => 'Failed to prepare query: ' . mysqli_error($conn), + 'has_data' => false + ]; + } + + mysqli_stmt_bind_param($stmt, "ssi", $startDateTime, $endDateTime, $limit); + $execResult = mysqli_stmt_execute($stmt); + + if (!$execResult) { + return [ + 'result' => false, + 'error' => 'Failed to execute query: ' . mysqli_stmt_error($stmt), + 'has_data' => false + ]; + } + + $result = mysqli_stmt_get_result($stmt); + $rowCount = mysqli_num_rows($result); + + // Get summary totals + $summaryQuery = " + SELECT + COUNT(DISTINCT dt.id_barang) as total_products, + SUM(dt.jumlah) as total_items_sold, + SUM(dt.total_harga) as total_revenue + FROM + detail_transaksi dt + JOIN + transaksi t ON dt.id_transaksi = t.id_transaksi + WHERE + t.tanggal BETWEEN ? AND ? + "; + + $summaryStmt = mysqli_prepare($conn, $summaryQuery); + + if (!$summaryStmt) { + return [ + 'result' => $result, + 'summary' => null, + 'error' => 'Failed to prepare summary query: ' . mysqli_error($conn), + 'has_data' => ($rowCount > 0) + ]; + } + + mysqli_stmt_bind_param($summaryStmt, "ss", $startDateTime, $endDateTime); + $summaryExecResult = mysqli_stmt_execute($summaryStmt); + + if (!$summaryExecResult) { + return [ + 'result' => $result, + 'summary' => null, + 'error' => 'Failed to execute summary query: ' . mysqli_stmt_error($summaryStmt), + 'has_data' => ($rowCount > 0) + ]; + } + + $summaryResult = mysqli_stmt_get_result($summaryStmt); + $summary = mysqli_fetch_assoc($summaryResult); + + // Handle case where no data is found + if (!$summary) { + $summary = [ + 'total_products' => 0, + 'total_items_sold' => 0, + 'total_revenue' => 0 + ]; + } + + // For non-admin users, potentially mask certain financial data + if (!$isAdmin) { + // Keep the summary accessible but hide financial details if needed + if (!canAccessFeature('view_financial')) { + $summary['total_revenue'] = 0; + } + } + + // Get category breakdown for pie chart - using jenis_barang table + $categoryQuery = " + SELECT + j.nama_jenis as kategori, + SUM(dt.jumlah) as total_quantity, + SUM(dt.total_harga) as total_revenue, + COUNT(DISTINCT b.id_barang) as unique_products + FROM + barang b + JOIN + jenis_barang j ON b.id_jenis = j.id_jenis + JOIN + detail_transaksi dt ON b.id_barang = dt.id_barang + JOIN + transaksi t ON dt.id_transaksi = t.id_transaksi + WHERE + t.tanggal BETWEEN ? AND ? + GROUP BY + j.nama_jenis + ORDER BY + total_quantity DESC + "; + + $categoryStmt = mysqli_prepare($conn, $categoryQuery); + + if (!$categoryStmt) { + return [ + 'result' => $result, + 'summary' => $summary, + 'categories' => null, + 'error' => 'Failed to prepare category query: ' . mysqli_error($conn), + 'has_data' => ($rowCount > 0) + ]; + } + + mysqli_stmt_bind_param($categoryStmt, "ss", $startDateTime, $endDateTime); + $categoryExecResult = mysqli_stmt_execute($categoryStmt); + + if (!$categoryExecResult) { + return [ + 'result' => $result, + 'summary' => $summary, + 'categories' => null, + 'error' => 'Failed to execute category query: ' . mysqli_stmt_error($categoryStmt), + 'has_data' => ($rowCount > 0) + ]; + } + + $categoryResult = mysqli_stmt_get_result($categoryStmt); + + return [ + 'result' => $result, + 'summary' => $summary, + 'categories' => $categoryResult, + 'has_data' => ($rowCount > 0) + ]; +} + +// Function to apply date ranges based on preset or manual selection +function getStartDate($datePresets, $activePreset) { + // If preset is selected, use preset dates + if (!empty($activePreset) && isset($datePresets[$activePreset])) { + return $datePresets[$activePreset]['start']; + } + + // Otherwise use the date parameters if provided + $startDate = isset($_GET['start_date']) && !empty($_GET['start_date']) + ? $_GET['start_date'] + : date('Y-m-01'); // Default to first day of current month + + // Make sure date is in YYYY-MM-DD format + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) { + $startDate = date('Y-m-01'); + } + + return $startDate; +} + +function getEndDate($datePresets, $activePreset) { + // If preset is selected, use preset dates + if (!empty($activePreset) && isset($datePresets[$activePreset])) { + return $datePresets[$activePreset]['end']; + } + + // Otherwise use the date parameters if provided + $endDate = isset($_GET['end_date']) && !empty($_GET['end_date']) + ? $_GET['end_date'] + : date('Y-m-d'); // Default to today + + // Make sure date is in YYYY-MM-DD format + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) { + $endDate = date('Y-m-d'); + } + + return $endDate; +} + +// Handle PDF export functionality +function exportToPDF($startDate, $endDate, $limit, $sortBy, $isAdmin) { + // Check permissions + if (!($isAdmin || canAccessFeature('export_pdf'))) { + die("Access denied. You don't have permission to export to PDF."); + } + + // Get the data + $queryData = getPopularProducts($startDate, $endDate, $limit, $sortBy, $isAdmin); + + // Here you would implement PDF generation with a library like FPDF or TCPDF + // For example: + /* + require('fpdf/fpdf.php'); + + $pdf = new FPDF(); + $pdf->AddPage(); + $pdf->SetFont('Arial', 'B', 16); + $pdf->Cell(40, 10, 'Ayula Store - Laporan Produk Terlaris'); + + // Add more PDF generation code here + + $pdf->Output('D', 'produk_terlaris.pdf'); + exit; + */ + + // For now, just return a message + echo "

PDF Export

"; + echo "

This functionality would generate a PDF for the selected date range: $startDate to $endDate

"; + echo "

Implementation with a PDF library is required to complete this feature.

"; + exit; +} \ No newline at end of file diff --git a/views/report/popular-products/popular-products.css b/views/report/popular-products/popular-products.css new file mode 100644 index 0000000..8beeb7c --- /dev/null +++ b/views/report/popular-products/popular-products.css @@ -0,0 +1,305 @@ +/* Custom styles for popular products report */ +.product-card { + transition: all 0.3s ease; + border-radius: 10px; + overflow: hidden; + margin-bottom: 20px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.05); + position: relative; + display: flex; + flex-direction: column; + height: 100%; +} + +.product-card:hover { + transform: translateY(-5px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); +} + +/* Move rank badge outside card body */ +.rank-badge { + position: absolute; + top: 10px; /* Slightly above card */ + left: 15px; + width: 40px; + height: 40px; + background-color: #7367f0; + color: white; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + font-weight: bold; + font-size: 18px; + z-index: 10; +} + +/* Special style for top 3 rankings */ +.top-3 { + background-color: #ff9f43; + font-size: 20px; +} + +/* Ensure card title and content don't overlap */ +.top-product .card-body { + padding-top: 60px; /* Push content below rank badge */ + padding-left: 15px; + padding-right: 15px; + position: relative; +} + +.top-product .card-title { + font-size: 16px; + margin: 0; + padding: 10px 0; + background-color: #f8f9fa; + border-bottom: 1px solid #e9ecef; + font-weight: 600; +} + +/* Add spacing between card content */ +.product-stats { + margin-top: 20px; /* Ensures enough space between content and stats */ +} + +/* Ensure the progress bar has some margin */ +.product-progress { + height: 8px; + margin-top: 10px; /* Space out progress bar from content */ +} + +/* Top products grid styles */ +.top-products-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 20px; + margin-top: 15px; +} + +/* Responsive adjustment */ +@media (max-width: 768px) { + .top-products-grid { + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + } + + .rank-badge { + width: 35px; + height: 35px; + font-size: 16px; + top: -10px; + } + + .product-card .card-body { + padding-top: 70px; + } +} + +/* Ensure cards are aligned properly on small screens */ +@media (max-width: 576px) { + .product-card .card { + min-height: 250px; + } +} + +/* Progress bar styles */ +.product-progress { + height: 8px; + margin-top: 5px; +} + +/* Top products grid styles */ +.top-products-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 20px; + margin-top: 15px; +} + +/* Memastikan card memiliki tinggi minimum */ +.product-card .card { + min-height: 200px; + height: 100%; + width: 100%; /* Pastikan lebar penuh */ + display: flex; + flex-direction: column; + margin-left: auto; /* Pindahkan kartu ke kanan dengan margin kiri otomatis */ + margin-right: 0; /* Menjaga margin kanan tidak tumpang tindih */ +} + +/* Custom styles for view toggle buttons */ +.view-toggle .btn { + padding: 0.25rem 0.5rem; +} + +.view-toggle .btn i { + margin-right: 0; +} + +/* Chart container styles */ +.chart-container { + position: relative; + height: 300px; + width: 100%; +} + +/* Employee-specific warning styles */ +.employee-warning { + display: none; +} + +.employee .employee-warning { + display: block; + padding: 5px 10px; + border-radius: 4px; + background-color: #fff8e1; + border-left: 4px solid #ffc107; + margin-bottom: 15px; +} + +/* Role-based styling */ +.admin-only { + display: none; +} + +.admin .admin-only { + display: block; +} + +.employee .sensitive-financial { + filter: blur(4px); + position: relative; +} + +.employee .sensitive-financial:hover::after { + content: "Data terbatas"; + position: absolute; + left: 0; + top: 0; + background: rgba(255,0,0,0.2); + padding: 2px 5px; + border-radius: 3px; + font-size: 10px; + white-space: nowrap; +} + +/* Disabled buttons for employees */ +.employee .disabled-for-employee { + opacity: 0.6; + pointer-events: none; +} + +.employee .request-only { + display: inline-block; +} + +.admin .request-only { + display: none; +} + +/* Styling untuk tabel view */ +#table-view th:first-child { + width: 70px; /* Lebar tetap untuk kolom ranking */ + text-align: center; +} + +#table-view .badge { + font-size: 14px; + padding: 5px 8px; +} + +/* Print specific styles */ +@media print { + .sidebar, .header, .no-print, #filter_inputs, .wordset, .search-set { + display: none !important; + } + + .page-wrapper { + margin-left: 0 !important; + padding: 0 !important; + } + + .card { + box-shadow: none !important; + margin-bottom: 1rem !important; + } + + .content { + padding: 0 !important; + } + + .table { + width: 100% !important; + } + + .table th, .table td { + padding: 0.25rem !important; + } + + .report-header { + display: block !important; + text-align: center; + margin-bottom: 20px; + } + + .report-header h2 { + margin-bottom: 5px; + } + + /* Force table view in print mode */ + #table-view { + display: block !important; + } + + #grid-view { + display: none !important; + } + + /* Hide chart canvases as they don't print well */ + canvas { + display: none !important; + } +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .top-products-grid { + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + } + + .dash-widget { + margin-bottom: 15px; + } + + .chart-container { + height: 250px; + } + + .rank-badge { + width: 30px; + height: 30px; + font-size: 14px; + } + + .rank-badge.top-3 { + width: 35px; + height: 35px; + font-size: 16px; + } + + .top-product .card-title { + font-size: 14px; + min-height: 40px; + } +} + +/* Make the table more compact on smaller screens */ +@media (max-width: 576px) { + .datanew th, .datanew td { + padding: 0.5rem 0.25rem; + font-size: 0.875rem; + } + + .view-toggle { + margin-left: auto; + } +} diff --git a/views/report/popular-products/popular-products.js b/views/report/popular-products/popular-products.js new file mode 100644 index 0000000..830dedc --- /dev/null +++ b/views/report/popular-products/popular-products.js @@ -0,0 +1,460 @@ +/** + * popular-products.js - Script for handling product popularity report functionality + * + * This script manages: + * - DataTable initialization + * - Date filtering and form handling + * - View toggling (grid vs table) + * - Export functionality (PDF, Excel) + * - Chart initialization + */ + +// Document ready function +$(document).ready(function () { + // Initialize datatable with error handling + initializeDataTable(); + + // Handle date preset selection + handleDatePresets(); + + // Handle limit and sort changes + handleFilterChanges(); + + // Form validation + setupFormValidation(); + + // View toggle + setupViewToggle(); + + // Export functionality + setupExportFunctionality(); + + // Initialize Chart.js charts if they exist + if (typeof initializeCharts === 'function') { + initializeCharts(); + } + + // Hide loading overlay when page is fully loaded + hideLoadingWithDelay(500); + + // Check user role for hiding UI elements + checkUserPermissions(); +}); + +/** + * Check user permissions and adjust UI accordingly + */ +function checkUserPermissions() { + // Check if body has class 'employee' or 'user' + if ($('body').hasClass('employee') || $('body').hasClass('user')) { + // Hide any admin-only elements that might not have been hidden by PHP + $('.admin-only').hide(); + } +} + +/** + * Initialize the DataTable with appropriate settings + */ +function initializeDataTable() { + try { + $('.datanew').DataTable({ + responsive: true, + language: { + search: 'Cari: _INPUT_', + searchPlaceholder: 'Cari produk...', + lengthMenu: 'Tampilkan: _MENU_', + paginate: { + 'first': 'Pertama', + 'last': 'Terakhir', + 'next': 'Berikutnya', + 'previous': 'Sebelumnya' + }, + info: "Menampilkan _START_ sampai _END_ dari _TOTAL_ entri", + infoEmpty: "Menampilkan 0 sampai 0 dari 0 entri", + infoFiltered: "(disaring dari _MAX_ entri total)", + emptyTable: "Tidak ada data tersedia untuk periode yang dipilih", + zeroRecords: "Tidak ditemukan catatan yang cocok" + }, + dom: '<"top"fl>rt<"bottom"ip><"clear">', + lengthMenu: [10, 25, 50, 100], + pageLength: 10, + drawCallback: function() { + hideLoadingOverlay(); + } + }); + } catch (e) { + console.error("DataTable initialization error:", e); + hideLoadingOverlay(); + } +} + +/** + * Handle date preset selection changes + */ +function handleDatePresets() { + $('#preset').on('change', function() { + if ($(this).val() === '') { + // Custom date range selected - show custom date inputs + $('#custom-date-inputs').show(); + } else { + // Preset selected - hide custom date inputs and show loading overlay + $('#custom-date-inputs').hide(); + showLoadingOverlay(); + + // Add a small delay before submitting to ensure loading overlay is visible + setTimeout(function() { + $('#date-filter-form').submit(); + }, 100); + } + }); +} + +/** + * Handle limit and sort_by changes (auto-submit form) + */ +function handleFilterChanges() { + $('#limit, #sort_by').on('change', function() { + showLoadingOverlay(); + + // Add a small delay before submitting + setTimeout(function() { + $('#date-filter-form').submit(); + }, 100); + }); + + // Handle reset button - show loading overlay + $('a[href="?reset=1"]').on('click', function() { + showLoadingOverlay(); + }); +} + +/** + * Setup form validation + */ +function setupFormValidation() { + $('#date-filter-form').on('submit', function(e) { + showLoadingOverlay(); + + // Only validate if custom date range is selected + if ($('#preset').val() === '') { + var startDate = $('#start_date').val(); + var endDate = $('#end_date').val(); + + if (!startDate || !endDate) { + alert('Harap pilih tanggal awal dan akhir'); + hideLoadingOverlay(); + e.preventDefault(); + return false; + } + + if (new Date(startDate) > new Date(endDate)) { + alert('Tanggal awal tidak boleh lebih besar dari tanggal akhir'); + hideLoadingOverlay(); + e.preventDefault(); + return false; + } + } + + return true; + }); +} + +/** + * Setup view toggle between grid and table + */ +function setupViewToggle() { + $('.view-toggle button').on('click', function() { + const viewType = $(this).data('view'); + + // Update active button + $('.view-toggle button').removeClass('active'); + $(this).addClass('active'); + + // Show/hide appropriate view + if (viewType === 'grid') { + $('#grid-view').show(); + $('#table-view').hide(); + } else { + $('#grid-view').hide(); + $('#table-view').show(); + } + }); +} + +/** + * Setup export functionality + */ +function setupExportFunctionality() { + // PDF Export + $('.pdf-export').on('click', function(e) { + e.preventDefault(); + + showLoadingOverlay(); + + // Check if the user has permission (controlled by CSS class) + if ($(this).hasClass('disabled-for-employee')) { + showPermissionModal('Ekspor PDF', 'Laporan ini tidak dapat diekspor ke PDF oleh akun karyawan.'); + hideLoadingOverlay(); + return; + } + + // If we have permission, proceed with export + exportToPDF(); + }); + + // Excel Export + $('.excel-export').on('click', function(e) { + e.preventDefault(); + + showLoadingOverlay(); + + // Check if the user has permission + if ($(this).hasClass('disabled-for-employee')) { + showPermissionModal('Ekspor Excel', 'Laporan ini tidak dapat diekspor ke Excel oleh akun karyawan.'); + hideLoadingOverlay(); + return; + } + + // If we have permission, proceed with export + exportToExcel(); + }); + + // Print Report + $('.print-report').on('click', function(e) { + e.preventDefault(); + + // Check if the user has permission + if ($(this).hasClass('disabled-for-employee')) { + showPermissionModal('Cetak Laporan', 'Pencetakan laporan dibatasi hanya untuk akun administrator.'); + return; + } + + // If we have permission, proceed with print + printReport(); + }); + + // For employee access - handle restricted action attempts + $('.employee-print, .employee-export').on('click', function(e) { + e.preventDefault(); + + // Get the action type from data attribute or default + var actionType = $(this).data('action') === 'print_attempt' ? 'Cetak Laporan' : 'Ekspor Laporan'; + var message = $(this).data('action') === 'print_attempt' + ? 'Pencetakan laporan dibatasi hanya untuk akun administrator.' + : 'Ekspor data dibatasi hanya untuk akun administrator.'; + + showPermissionModal(actionType, message); + }); +} + +/** + * Show permission denied modal + */ +function showPermissionModal(actionType, message) { + // Set the action details + $('#actionDetails').html( + 'Tindakan yang Dicoba: ' + actionType + '
' + + '' + message + '' + ); + + // Show the modal + var permissionModal = new bootstrap.Modal(document.getElementById('permissionModal')); + permissionModal.show(); +} + +/** + * Print the current report + */ +function printReport() { + // Add print-specific styling + $(' + + + +
+
+
+ + +
+
+
Memuat data...
+
+ +
+
+ + + + + + + + + + + + + +
+ + + +
+
+ + + +
+
+
+
+ +
+
+
Total
+
Transaksi
+
+
+
+
+
+
+ +
+
+
Total
+
Item Terjual
+
+
+
+ +
+
+
+ +
+
+
Rp.
+
Total Penjualan
+
+
+
+ + +
+
+
+ +
+
+
Laporan Aktivitas
+
Hubungi admin untuk detail keuangan
+
+
+
+ +
+ + +
+
+
Filter Tanggal
+
+
+
+
+
+
+ + +
+
+ + + +
+
> +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+ +
+
+ + Reset + + + + + Cetak + + + + Cetak + + + + +
+ + +
+ + + + Ekspor + + +
+
+
+
+
+ + +
+ Rentang Tanggal Saat Ini: sampai + + 'Hari Ini', + 'yesterday' => 'Kemarin', + 'this_week' => 'Minggu Ini', + 'last_week' => 'Minggu Lalu', + 'this_month' => 'Bulan Ini', + 'last_30_days' => '30 Hari Terakhir', + 'last_month' => 'Bulan Lalu', + 'last_90_days' => '90 Hari Terakhir', + 'last_quarter' => 'Kuartal Terakhir', + 'last_year' => 'Tahun Lalu', + 'all_time' => 'Sepanjang Waktu' + ]; + $label = isset($presetLabels[$activePreset]) ? $presetLabels[$activePreset] : $datePresets[$activePreset]['label']; + ?> + + +
+ +
+
+ + +
+
+
Transaksi Penjualan
+
+
+ + +
+
+ +
+

Tidak ada transaksi ditemukan untuk periode yang dipilih

+

+ + Tidak ada transaksi penjualan yang tercatat untuk tahun lalu (). + + Coba pilih rentang tanggal atau preset tanggal yang berbeda. + +

+ + Reset Filter + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + 0): ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID TransaksiTanggalItemProdukTotalTunaiKembalianStatusAksi
Rp. Rp. Rp. Selesai + + Lihat + + + + Cetak + + +
Tidak ada transaksi ditemukan untuk periode yang dipilih
+
+ +
+
+ + + +
+
+
+
+
Ringkasan untuk Periode yang Dipilih
+
-
+
+
+
+
+
+
+ Total Transaksi: + +
+
+ Total Item Terjual: + +
+
+
+
+ Total: + Rp. +
+
+ Rata-rata Harian: + Rp. 0 ? $totals['grand_total'] / $daysDiff : 0; + echo number_format($dailyAvg); + ?> +
+
+
+
+
+
+
+
+ + + + +
+
+
+
+
Ringkasan Aktivitas Anda
+
+
+
+ + Untuk laporan keuangan yang lebih rinci atau untuk mengekspor data, silakan hubungi administrator Anda. +
+ +
+
+
+
+ Transaksi Dilihat: + +
+
+ Item Diproses: + +
+
+
+
+ Laporan Dibuat: + +
+
+ Level Akses: + Karyawan +
+
+
+
+
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/views/report/sales-report/salesreport-config.php b/views/report/sales-report/salesreport-config.php new file mode 100644 index 0000000..e6f2206 --- /dev/null +++ b/views/report/sales-report/salesreport-config.php @@ -0,0 +1,515 @@ + [ + 'label' => 'Hari Ini', + 'start' => date('Y-m-d'), + 'end' => date('Y-m-d') + ], + 'yesterday' => [ + 'label' => 'Kemarin', + 'start' => date('Y-m-d', strtotime('-1 day')), + 'end' => date('Y-m-d', strtotime('-1 day')) + ], + 'this_week' => [ + 'label' => 'Minggu Ini', + 'start' => date('Y-m-d', strtotime('monday this week')), + 'end' => date('Y-m-d') + ], + 'last_week' => [ + 'label' => 'Minggu Lalu', + 'start' => date('Y-m-d', strtotime('monday last week')), + 'end' => date('Y-m-d', strtotime('sunday last week')) + ], + 'this_month' => [ + 'label' => 'Bulan Ini', + 'start' => date('Y-m-01'), + 'end' => date('Y-m-d') + ], + ]; + + // Preset hanya untuk admin + if ($isAdmin) { + $presets['last_month'] = [ + 'label' => 'Bulan Lalu', + 'start' => date('Y-m-d', strtotime('first day of last month')), + 'end' => date('Y-m-d', strtotime('last day of last month')) + ]; + + $presets['last_90_days'] = [ + 'label' => '90 Hari Terakhir', + 'start' => date('Y-m-d', strtotime('-90 days')), + 'end' => date('Y-m-d') + ]; + + $presets['all_time'] = [ + 'label' => 'Sepanjang Waktu', + 'start' => '2000-01-01', // Tetapkan tanggal mulai yang masuk akal + 'end' => date('Y-m-d') + ]; + } + + return $presets; +} + +// Dapatkan preset tanggal yang sesuai berdasarkan peran pengguna +$isAdmin = isAdmin(); +$datePresets = getDatePresets($isAdmin); + +// Tangani permintaan reset +if (isset($_GET['reset']) && $_GET['reset'] == 1) { + // Jika reset diminta, alihkan ke halaman yang sama dengan tanggal default + header("Location: index.php"); + exit; +} + +// Jika pengguna bukan admin, batasi akses data historis +if (!$isAdmin && empty($_GET['preset']) && empty($_GET['start_date'])) { + // Default pengguna non-admin ke minggu ini jika tidak ada tanggal yang ditentukan + $_GET['preset'] = 'this_week'; +} + +// Periksa apakah preset dipilih +$activePreset = isset($_GET['preset']) && !empty($_GET['preset']) ? $_GET['preset'] : ''; + +// Terapkan rentang tanggal berdasarkan preset atau pemilihan manual +function getStartDate() { + global $datePresets, $activePreset; + + // Jika preset dipilih, gunakan tanggal preset + if (!empty($activePreset) && isset($datePresets[$activePreset])) { + return $datePresets[$activePreset]['start']; + } + + // Jika tidak, gunakan parameter tanggal jika disediakan + $startDate = isset($_GET['start_date']) && !empty($_GET['start_date']) + ? $_GET['start_date'] + : date('Y-m-01'); // Default ke hari pertama bulan ini + + // Pastikan tanggal dalam format YYYY-MM-DD + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) { + $startDate = date('Y-m-01'); + } + + return $startDate; +} + +function getEndDate() { + global $datePresets, $activePreset; + + // Jika preset dipilih, gunakan tanggal preset + if (!empty($activePreset) && isset($datePresets[$activePreset])) { + return $datePresets[$activePreset]['end']; + } + + // Jika tidak, gunakan parameter tanggal jika disediakan + $endDate = isset($_GET['end_date']) && !empty($_GET['end_date']) + ? $_GET['end_date'] + : date('Y-m-d'); // Default ke hari ini + + // Pastikan tanggal dalam format YYYY-MM-DD + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) { + $endDate = date('Y-m-d'); + } + + return $endDate; +} + +$startDate = getStartDate(); +$endDate = getEndDate(); + +// Pastikan tanggal mulai tidak setelah tanggal akhir +if (strtotime($startDate) > strtotime($endDate)) { + $temp = $startDate; + $startDate = $endDate; + $endDate = $temp; +} + +// Fungsi untuk mendapatkan kueri yang sesuai dengan peran dengan penanganan tidak ada data yang lebih baik +function getReportQueries($startDate, $endDate, $isAdmin = false) { + global $conn; + + // Tambahkan komponen waktu untuk membuat rentang tanggal inklusif + $startDateTime = $startDate . ' 00:00:00'; + $endDateTime = $endDate . ' 23:59:59'; + + // Siapkan kueri transaksi dasar dengan filter tanggal + $baseQuery = "SELECT t.id_transaksi, t.kode_transaksi, t.tanggal, t.total_item, + t.subtotal, t.total, t.metode_pembayaran, t.cash_amount, t.change_amount, + COUNT(DISTINCT dt.id_barang) as total_products + FROM transaksi t + LEFT JOIN detail_transaksi dt ON t.id_transaksi = dt.id_transaksi + WHERE t.tanggal BETWEEN ? AND ?"; + + // Admin melihat semuanya + if ($isAdmin) { + $transactionQuery = $baseQuery . " GROUP BY t.id_transaksi ORDER BY t.tanggal DESC"; + } + // Karyawan melihat data terbatas dan hanya transaksi terbaru + else { + // Untuk karyawan, kami membatasi jumlah catatan + $transactionQuery = $baseQuery . " GROUP BY t.id_transaksi ORDER BY t.tanggal DESC LIMIT 100"; + } + + // Siapkan dan jalankan kueri transaksi + $stmt = mysqli_prepare($conn, $transactionQuery); + + if (!$stmt) { + // Tangani kesalahan persiapan kueri + return [ + 'result' => false, + 'paymentResult' => null, + 'totals' => [ + 'total_transactions' => 0, + 'total_items' => 0, + 'total_subtotal' => 0, + 'grand_total' => 0 + ], + 'error' => 'Gagal menyiapkan kueri: ' . mysqli_error($conn), + 'has_data' => false + ]; + } + + mysqli_stmt_bind_param($stmt, "ss", $startDateTime, $endDateTime); + $execResult = mysqli_stmt_execute($stmt); + + if (!$execResult) { + // Tangani kesalahan eksekusi kueri + return [ + 'result' => false, + 'paymentResult' => null, + 'totals' => [ + 'total_transactions' => 0, + 'total_items' => 0, + 'total_subtotal' => 0, + 'grand_total' => 0 + ], + 'error' => 'Gagal mengeksekusi kueri: ' . mysqli_stmt_error($stmt), + 'has_data' => false + ]; + } + + $result = mysqli_stmt_get_result($stmt); + $rowCount = mysqli_num_rows($result); + + // Dapatkan total untuk ringkasan + $totalQuery = "SELECT + COUNT(id_transaksi) as total_transactions, + SUM(total_item) as total_items, + SUM(subtotal) as total_subtotal, + SUM(total) as grand_total + FROM transaksi + WHERE tanggal BETWEEN ? AND ?"; + + $totalStmt = mysqli_prepare($conn, $totalQuery); + + if (!$totalStmt) { + // Tangani kesalahan persiapan kueri total + return [ + 'result' => $result, + 'paymentResult' => null, + 'totals' => [ + 'total_transactions' => 0, + 'total_items' => 0, + 'total_subtotal' => 0, + 'grand_total' => 0 + ], + 'error' => 'Gagal menyiapkan kueri total: ' . mysqli_error($conn), + 'has_data' => ($rowCount > 0) + ]; + } + + mysqli_stmt_bind_param($totalStmt, "ss", $startDateTime, $endDateTime); + $totalExecResult = mysqli_stmt_execute($totalStmt); + + if (!$totalExecResult) { + // Tangani kesalahan eksekusi kueri total + return [ + 'result' => $result, + 'paymentResult' => null, + 'totals' => [ + 'total_transactions' => 0, + 'total_items' => 0, + 'total_subtotal' => 0, + 'grand_total' => 0 + ], + 'error' => 'Gagal mengeksekusi kueri total: ' . mysqli_stmt_error($totalStmt), + 'has_data' => ($rowCount > 0) + ]; + } + + $totalResult = mysqli_stmt_get_result($totalStmt); + $totals = mysqli_fetch_assoc($totalResult); + + // Tangani kasus di mana tidak ada data yang ditemukan + if (!$totals) { + $totals = [ + 'total_transactions' => 0, + 'total_items' => 0, + 'total_subtotal' => 0, + 'grand_total' => 0 + ]; + } + + // Untuk pengguna non-admin, masker data keuangan tertentu + if (!$isAdmin && $totals) { + // Hanya tampilkan kuantitas, sembunyikan angka keuangan + if (!canAccessFeature('view_financial')) { + $totals['total_subtotal'] = 0; + $totals['grand_total'] = 0; + } + } + + // Dapatkan breakdown metode pembayaran - hanya untuk pengguna dengan izin yang tepat + $paymentResult = null; + if (($isAdmin || canAccessFeature('view_payment_methods')) && $rowCount > 0) { + $paymentQuery = "SELECT + metode_pembayaran, + COUNT(*) as count, + SUM(total) as total_amount + FROM transaksi + WHERE tanggal BETWEEN ? AND ? + GROUP BY metode_pembayaran + ORDER BY total_amount DESC"; + + $paymentStmt = mysqli_prepare($conn, $paymentQuery); + if ($paymentStmt) { + mysqli_stmt_bind_param($paymentStmt, "ss", $startDateTime, $endDateTime); + $paymentExecResult = mysqli_stmt_execute($paymentStmt); + + if ($paymentExecResult) { + $paymentResult = mysqli_stmt_get_result($paymentStmt); + } + } + } + + return [ + 'result' => $result, + 'paymentResult' => $paymentResult, + 'totals' => $totals, + 'has_data' => ($rowCount > 0) + ]; +} + +// Fungsi untuk memeriksa apakah karyawan memiliki izin untuk fitur laporan tertentu +function canAccessFeature($feature) { + // Izin default berdasarkan peran + $permissions = [ + 'admin' => [ + 'export_excel' => true, + 'export_pdf' => true, + 'print_report' => true, + 'view_financial' => true, + 'view_payment_methods' => true, + 'view_all_time' => true, + 'view_summary' => true + ], + 'manager' => [ + 'export_excel' => true, + 'export_pdf' => false, + 'print_report' => true, + 'view_financial' => true, + 'view_payment_methods' => true, + 'view_all_time' => false, + 'view_summary' => true + ], + 'employee' => [ + 'export_excel' => false, + 'export_pdf' => false, + 'print_report' => false, + 'view_financial' => false, + 'view_payment_methods' => false, + 'view_all_time' => false, + 'view_summary' => false + ] + ]; + + $role = getUserRole(); + + // Jika peran tidak ada dalam izin kami, default ke karyawan + if (!isset($permissions[$role])) { + $role = 'employee'; + } + + // Kembalikan status izin + return isset($permissions[$role][$feature]) ? $permissions[$role][$feature] : false; +} + +// Dapatkan kueri yang sesuai berdasarkan peran pengguna +$queryData = getReportQueries($startDate, $endDate, $isAdmin); +$result = $queryData['result']; +$paymentResult = $queryData['paymentResult']; +$totals = $queryData['totals']; + +// Dapatkan peran pengguna dari sesi untuk izin menu +$userRole = getUserRole(); + +// Menangani permintaan ekspor PDF +if (isset($_POST['export_pdf']) || (isset($_GET['export']) && $_GET['export'] == 'pdf')) { + // Periksa izin + if (!($isAdmin || canAccessFeature('export_pdf'))) { + die("Akses ditolak. Anda tidak memiliki izin untuk mengekspor ke PDF."); + } + + // Dapatkan parameter tanggal + $exportStartDate = isset($_POST['start_date']) ? $_POST['start_date'] : (isset($_GET['start_date']) ? $_GET['start_date'] : $startDate); + $exportEndDate = isset($_POST['end_date']) ? $_POST['end_date'] : (isset($_GET['end_date']) ? $_GET['end_date'] : $endDate); + + // Tetapkan header untuk PDF (memaksa unduhan) + header('Content-Type: text/html; charset=utf-8'); + + // Mulai output buffering + ob_start(); + + // Konten HTML ramah PDF + echo ''; + echo ''; + echo ''; + echo ''; + echo 'Laporan Penjualan'; + echo ''; + echo ''; + echo ''; + + // Tambahkan tombol cetak + echo '
'; + echo ''; + echo '

Setelah mencetak, gunakan dialog cetak browser Anda untuk menyimpan sebagai PDF

'; + echo '
'; + + // Header laporan + echo '
'; + echo '

Ayula Store - Laporan Penjualan

'; + echo '

Periode: ' . date('d M Y', strtotime($exportStartDate)) . ' sampai ' . date('d M Y', strtotime($exportEndDate)) . '

'; + echo '

Dibuat pada: ' . date('d M Y H:i:s') . '

'; + echo '

Dibuat oleh: ' . ($userRole) . '

'; + echo '
'; + + // Jalankan kembali kueri untuk mendapatkan data baru + $exportQueryData = getReportQueries($exportStartDate, $exportEndDate, true); + $exportResult = $exportQueryData['result']; + $exportTotals = $exportQueryData['totals']; + + // Bagian ringkasan + echo '
'; + echo '

Ringkasan

'; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + $daysDiff = (strtotime($exportEndDate) - strtotime($exportStartDate)) / (60 * 60 * 24) + 1; + $dailyAvg = $daysDiff > 0 ? $exportTotals['grand_total'] / $daysDiff : 0; + echo ''; + echo '
Total Transaksi' . number_format($exportTotals['total_transactions']) . '
Total Item Terjual' . number_format($exportTotals['total_items']) . '
SubtotalRp. ' . number_format($exportTotals['total_subtotal']) . '
TotalRp. ' . number_format($exportTotals['grand_total']) . '
Rata-rata HarianRp. ' . number_format($dailyAvg) . '
'; + echo '
'; + + // Tabel transaksi + echo '

Transaksi

'; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + if ($exportResult && mysqli_num_rows($exportResult) > 0) { + mysqli_data_seek($exportResult, 0); // Reset pointer ke awal set hasil + while ($row = mysqli_fetch_assoc($exportResult)) { + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + } else { + echo ''; + } + + echo ''; + echo '
ID TransaksiTanggalItemSubtotalTotalPembayaran
' . $row['kode_transaksi'] . '' . date('d M Y H:i', strtotime($row['tanggal'])) . '' . $row['total_item'] . 'Rp. ' . number_format($row['subtotal']) . 'Rp. ' . number_format($row['total']) . '' . $row['metode_pembayaran'] . '
Tidak ada transaksi ditemukan
'; + + // Footer + echo ''; + + echo ''; + echo ''; + + // Keluarkan buffer dan akhiri + ob_end_flush(); + exit; +} +?> \ No newline at end of file diff --git a/views/report/sales-report/salesreport.css b/views/report/sales-report/salesreport.css new file mode 100644 index 0000000..67c3ab3 --- /dev/null +++ b/views/report/sales-report/salesreport.css @@ -0,0 +1,270 @@ +/* Custom styles for toolbar */ +.multi-select-toolbar { + position: fixed; + top: 0; /* Position at the top */ + left: 0; + width: 100%; + background-color: #ff9f43; /* Brand color */ + color: #ffffff; /* White text */ + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + padding: 15px; + display: none; + z-index: 9999; /* Highest z-index to ensure it's above everything else */ + justify-content: space-between; + align-items: center; +} + +.multi-select-toolbar.active { + display: flex; +} + +/* Adjust all other page content when toolbar is active */ +body.toolbar-active .header { + margin-top: 60px; /* Height of toolbar + padding */ +} + +body.toolbar-active .sidebar { + padding-top: 60px; /* Push sidebar content down */ +} + +body.toolbar-active .page-wrapper { + padding-top: 60px; /* Push main content down when toolbar is visible */ +} + +/* Make sure text is fully visible in toolbar */ +.multi-select-toolbar .selected-count { + font-weight: bold; + padding: 5px; + margin: 0; +} + +.multi-select-toolbar .toolbar-actions { + display: flex; + gap: 10px; +} + +.multi-select-toolbar .toolbar-actions button { + white-space: nowrap; + padding: 8px 15px; + font-size: 14px; +} +/* Ensure the date picker stays on top */ +.datetimepicker { + z-index: 999999 !important; /* Ensure it's on top of other elements */ +} + +/* Custom styles for dashboard stats */ +.dash-widget-icon { + width: 50px; + height: 50px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + font-size: 24px; + background-color: rgba(255, 159, 67, 0.2); + color: #ff9f43; +} + +.dash-count { + font-size: 20px; + font-weight: 700; + margin-bottom: 10px; +} + +.dash-widget { + transition: all 0.3s ease; +} + +.dash-widget:hover { + transform: translateY(-5px); + box-shadow: 0 5px 15px rgba(0,0,0,0.1); +} + +/* Report specific styles */ +.report-card { + border-radius: 10px; + overflow: hidden; + box-shadow: 0 5px 15px rgba(0,0,0,0.05); +} + +.report-card .card-header { + background-color: #f9f9f9; + border-bottom: 1px solid #eee; +} + +.report-summary { + background-color: #f8fbff; + padding: 15px; + border-radius: 8px; + margin-bottom: 20px; +} + +.report-summary-item { + display: flex; + justify-content: space-between; + padding: 8px 0; + border-bottom: 1px dotted #ddd; +} + +.report-summary-item:last-child { + border-bottom: none; +} + +/* Role-based styling */ +.admin-only { + display: none; +} + +.admin .admin-only { + display: block; +} + +.employee .sensitive-financial { + filter: blur(4px); + position: relative; +} + +.employee .sensitive-financial:hover::after { + content: "Restricted data"; + position: absolute; + left: 0; + top: 0; + background: rgba(255,0,0,0.2); + padding: 2px 5px; + border-radius: 3px; + font-size: 10px; + white-space: nowrap; +} + +/* Employee specific warning */ +.employee-warning { + display: none; +} + +.employee .employee-warning { + display: block; + padding: 5px 10px; + border-radius: 4px; + background-color: #fff8e1; + border-left: 4px solid #ffc107; + margin-bottom: 15px; +} + +/* Disabled buttons for employees */ +.employee .disabled-for-employee { + opacity: 0.6; + pointer-events: none; +} + +.employee .request-only { + display: inline-block; +} + +.admin .request-only { + display: none; +} + +/* Modal styling for employee permissions */ +#permissionModal .modal-header { + border-bottom: 3px solid #dc3545; + background-color: #fff8f8; + color: #dc3545; +} + +#permissionModal .modal-body { + padding: 25px; +} + +#permissionModal .modal-footer { + border-top: 1px solid #eee; + background-color: #f9f9f9; +} + +#permissionModal .fas.fa-lock { + color: #dc3545; + animation: pulse 1.5s ease-in-out infinite; +} + +#permissionModal .btn-secondary { + background-color: #6c757d; + border-color: #6c757d; +} + +#permissionModal .btn-secondary:hover { + background-color: #5a6268; + border-color: #545b62; +} + +#permissionModal #actionDetails { + background-color: #f8f9fa; + border-left: 4px solid #dc3545; + padding: 10px 15px; + font-size: 0.9rem; +} + +/* Animations */ +@keyframes pulse { + 0% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.1); + opacity: 0.8; + } + 100% { + transform: scale(1); + opacity: 1; + } +} + +@keyframes fadeInScale { + from { + transform: scale(0.5); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} + +/* Print specific styles */ +@media print { + .sidebar, .header, .no-print, #filter_inputs, .wordset, .search-set { + display: none !important; + } + + .page-wrapper { + margin-left: 0 !important; + padding: 0 !important; + } + + .card { + box-shadow: none !important; + margin-bottom: 1rem !important; + } + + .content { + padding: 0 !important; + } + + .table { + width: 100% !important; + } + + .table th, .table td { + padding: 0.25rem !important; + } + + .report-header { + display: block !important; + text-align: center; + margin-bottom: 20px; + } + + .report-header h2 { + margin-bottom: 5px; + } +} \ No newline at end of file diff --git a/views/report/sales-report/salesreport.js b/views/report/sales-report/salesreport.js new file mode 100644 index 0000000..a94ba5b --- /dev/null +++ b/views/report/sales-report/salesreport.js @@ -0,0 +1,358 @@ +// salesreport.js - Versi yang ditingkatkan dengan penanganan tidak ada data +$(document).ready(function () { + // Inisialisasi datatable dengan penanganan kesalahan + try { + $('.datanew').DataTable({ + responsive: true, + language: { + search: 'Cari: _INPUT_', + searchPlaceholder: 'Cari transaksi...', + lengthMenu: 'Tampilkan: _MENU_', + paginate: { + 'first': 'Pertama', + 'last': 'Terakhir', + 'next': '>', + 'previous': '<' + }, + info: "Menampilkan _START_ sampai _END_ dari _TOTAL_ entri", + infoEmpty: "Menampilkan 0 sampai 0 dari 0 entri", + infoFiltered: "(disaring dari _MAX_ entri total)", + emptyTable: "Tidak ada data tersedia untuk periode yang dipilih", + zeroRecords: "Tidak ditemukan catatan yang cocok", + processing: "Memproses..." + }, + dom: '<"top"fl>rt<"bottom"ip><"clear">', + lengthMenu: [10, 25, 50, 100], + pageLength: 10, + // Penanganan kesalahan untuk DataTables + drawCallback: function() { + // Sembunyikan loading overlay saat data siap + if (document.getElementById('loading-overlay')) { + document.getElementById('loading-overlay').style.display = 'none'; + } + } + }); + } catch (e) { + console.error("Kesalahan inisialisasi DataTable:", e); + // Sembunyikan loading overlay jika terjadi kesalahan + if (document.getElementById('loading-overlay')) { + document.getElementById('loading-overlay').style.display = 'none'; + } + } + + // Menangani pemilihan preset tanggal + $('#preset').on('change', function() { + if ($(this).val() === '') { + // Rentang tanggal kustom dipilih - tampilkan input tanggal kustom + $('#custom-date-inputs').show(); + } else { + // Preset dipilih - sembunyikan input tanggal kustom dan tampilkan loading overlay + $('#custom-date-inputs').hide(); + + // Tampilkan loading overlay sebelum mengirimkan + if (document.getElementById('loading-overlay')) { + document.getElementById('loading-overlay').style.display = 'flex'; + } + + // Tambahkan penundaan kecil sebelum mengirimkan untuk memastikan loading overlay terlihat + setTimeout(function() { + $('#date-filter-form').submit(); + }, 100); + } + }); + + // Validasi form dasar + $('#date-filter-form').on('submit', function(e) { + // Tampilkan loading overlay + if (document.getElementById('loading-overlay')) { + document.getElementById('loading-overlay').style.display = 'flex'; + } + + // Hanya validasi jika rentang tanggal kustom dipilih + if ($('#preset').val() === '') { + var startDate = $('#start_date').val(); + var endDate = $('#end_date').val(); + + if (!startDate || !endDate) { + alert('Silakan pilih tanggal mulai dan tanggal akhir'); + // Sembunyikan loading overlay jika validasi gagal + if (document.getElementById('loading-overlay')) { + document.getElementById('loading-overlay').style.display = 'none'; + } + e.preventDefault(); + return false; + } + + if (new Date(startDate) > new Date(endDate)) { + alert('Tanggal mulai tidak boleh lebih besar dari tanggal akhir'); + // Sembunyikan loading overlay jika validasi gagal + if (document.getElementById('loading-overlay')) { + document.getElementById('loading-overlay').style.display = 'none'; + } + e.preventDefault(); + return false; + } + } + + return true; + }); + + // Fungsi ekspor + $('.pdf-export').on('click', function(e) { + e.preventDefault(); + + // Tampilkan loading overlay + if (document.getElementById('loading-overlay')) { + document.getElementById('loading-overlay').style.display = 'flex'; + } + + // Periksa apakah pengguna memiliki izin (dikontrol oleh kelas CSS) + if ($(this).hasClass('disabled-for-employee')) { + showPermissionModal('Ekspor PDF', 'Laporan ini tidak dapat diekspor ke PDF oleh akun karyawan.'); + // Sembunyikan loading overlay jika pemeriksaan izin gagal + if (document.getElementById('loading-overlay')) { + document.getElementById('loading-overlay').style.display = 'none'; + } + return; + } + + // Jika kita memiliki izin, lanjutkan dengan ekspor + exportTableToPDF(); + }); + + $('.excel-export').on('click', function(e) { + e.preventDefault(); + + // Tampilkan loading overlay + if (document.getElementById('loading-overlay')) { + document.getElementById('loading-overlay').style.display = 'flex'; + } + + // Periksa apakah pengguna memiliki izin (dikontrol oleh kelas CSS) + if ($(this).hasClass('disabled-for-employee')) { + showPermissionModal('Ekspor Excel', 'Laporan ini tidak dapat diekspor ke Excel oleh akun karyawan.'); + // Sembunyikan loading overlay jika pemeriksaan izin gagal + if (document.getElementById('loading-overlay')) { + document.getElementById('loading-overlay').style.display = 'none'; + } + return; + } + + // Jika kita memiliki izin, lanjutkan dengan ekspor + exportTableToExcel(); + }); + + $('.print-report').on('click', function(e) { + e.preventDefault(); + + // Periksa apakah pengguna memiliki izin (dikontrol oleh kelas CSS) + if ($(this).hasClass('disabled-for-employee')) { + showPermissionModal('Cetak Laporan', 'Pencetakan laporan dibatasi hanya untuk akun administrator.'); + return; + } + + // Jika kita memiliki izin, lanjutkan dengan cetak + printReport(); + }); + + // Untuk akses karyawan - menangani upaya tindakan terbatas + $('.employee-print, .employee-export').on('click', function(e) { + e.preventDefault(); + + // Dapatkan jenis tindakan dari atribut data atau default + var actionType = $(this).data('action') === 'print_attempt' ? 'Cetak Laporan' : 'Ekspor Laporan'; + var message = $(this).data('action') === 'print_attempt' + ? 'Pencetakan laporan dibatasi hanya untuk akun administrator.' + : 'Ekspor data dibatasi hanya untuk akun administrator.'; + + showPermissionModal(actionType, message); + }); + + // Menangani tombol reset - tampilkan loading overlay + $('a[href="?reset=1"]').on('click', function() { + if (document.getElementById('loading-overlay')) { + document.getElementById('loading-overlay').style.display = 'flex'; + } + }); +}); + +// Fungsi untuk menampilkan modal izin +function showPermissionModal(actionType, message) { + // Atur detail tindakan + $('#actionDetails').html( + 'Tindakan yang Dicoba: ' + actionType + '
' + + '' + message + '' + ); + + // Tampilkan modal + var permissionModal = new bootstrap.Modal(document.getElementById('permissionModal')); + permissionModal.show(); +} + +// Fungsi untuk mencetak tanda terima +function printReceipt(transactionId) { + // Buka tanda terima di jendela baru untuk pencetakan + var printWindow = window.open('../../transaction/transaction_success.php?id=' + transactionId + '&print=true', '_blank', 'width=400,height=600'); + + // Cetak otomatis setelah konten dimuat + printWindow.onload = function () { + setTimeout(function () { + printWindow.print(); + }, 500); + }; + + return false; +} + +// Fungsi untuk mencetak laporan saat ini +function printReport() { + // Tambahkan gaya khusus untuk pencetakan + $(' + + + ${printContent} + + + `); + printWindow.document.close(); + + setTimeout(function() { + printWindow.print(); + printWindow.close(); + }, 500); +} + +// Function to export report (PDF or Excel) +function exportReport(type) { + // Get current filter values + const filterForm = $('#report-filter-form'); + const filterType = $('#filter-type').val(); + const startDate = $('#start_date').val(); + const endDate = $('#end_date').val(); + const filterMonth = $('select[name="filter_month"]').val(); + const filterYear = $('select[name="filter_year"]').val(); + const productId = $('select[name="product_id"]').val(); + const categoryId = $('select[name="category_id"]').val(); + + // Construct URL with parameters + let url = 'report_export.php?export=' + type; + + if (filterType === 'date_range') { + url += '&filter_type=date_range&start_date=' + startDate + '&end_date=' + endDate; + } else if (filterType === 'month') { + url += '&filter_type=month&filter_month=' + filterMonth + '&filter_year=' + filterYear; + } else if (filterType === 'year') { + url += '&filter_type=year&filter_year=' + filterYear; + } + + if (productId) { + url += '&product_id=' + productId; + } + + if (categoryId) { + url += '&category_id=' + categoryId; + } + + // Open in new window/tab + window.open(url, '_blank'); +} + +// Initialize Category Distribution Chart +function initCategoryChart() { + const categoryCtx = document.getElementById('categoryChart'); + + if (!categoryCtx) return; + + // Destroy existing chart if it exists + if (window.categoryChart) { + window.categoryChart.destroy(); + } + + window.categoryChart = new Chart(categoryCtx.getContext('2d'), { + type: 'pie', + data: { + labels: [], + datasets: [{ + label: 'Jumlah Items', + data: [], + backgroundColor: [ + 'rgba(255, 99, 132, 0.7)', + 'rgba(54, 162, 235, 0.7)', + 'rgba(255, 206, 86, 0.7)', + 'rgba(75, 192, 192, 0.7)', + 'rgba(153, 102, 255, 0.7)', + 'rgba(255, 159, 64, 0.7)', + 'rgba(199, 199, 199, 0.7)', + 'rgba(83, 102, 255, 0.7)', + 'rgba(40, 159, 64, 0.7)', + 'rgba(210, 199, 199, 0.7)' + ], + borderWidth: 1 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'right', + }, + tooltip: { + callbacks: { + label: function(context) { + const label = context.label || ''; + const value = context.raw || 0; + const total = context.dataset.data.reduce((a, b) => a + b, 0); + const percentage = Math.round((value / total) * 100); + return `${label}: ${value} (${percentage}%)`; + } + } + } + } + } + }); + + // Load initial data + const labels = []; + const data = []; + + // Get data from PHP-rendered elements with special data attributes + const dataElements = document.querySelectorAll('[data-category-name]'); + dataElements.forEach(element => { + labels.push(element.getAttribute('data-category-name')); + data.push(parseInt(element.getAttribute('data-category-count'))); + }); + + // If no data elements found, try to parse from the script + if (labels.length === 0) { + try { + // This assumes there's PHP-rendered data in the original script + const categoryData = JSON.parse(document.getElementById('category-data').textContent); + categoryData.forEach(item => { + labels.push(item.name); + data.push(item.count); + }); + } catch (e) { + console.warn('Could not load category chart data', e); + } + } + + // Update chart with data + window.categoryChart.data.labels = labels; + window.categoryChart.data.datasets[0].data = data; + window.categoryChart.update(); +} + +// Initialize Value Distribution Chart +function initValueChart() { + const valueCtx = document.getElementById('valueChart'); + + if (!valueCtx) return; + + // Destroy existing chart if it exists + if (window.valueChart) { + window.valueChart.destroy(); + } + + window.valueChart = new Chart(valueCtx.getContext('2d'), { + type: 'bar', + data: { + labels: [], + datasets: [{ + label: 'Nilai (Rp)', + data: [], + backgroundColor: 'rgba(54, 162, 235, 0.6)', + borderColor: 'rgba(54, 162, 235, 1)', + borderWidth: 1 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + y: { + beginAtZero: true, + ticks: { + callback: function(value) { + return 'Rp ' + value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, "."); + } + } + } + }, + plugins: { + tooltip: { + callbacks: { + label: function(context) { + let label = context.dataset.label || ''; + if (label) { + label += ': '; + } + if (context.parsed.y !== null) { + label += 'Rp ' + context.parsed.y.toString().replace(/\B(?=(\d{3})+(?!\d))/g, "."); + } + return label; + } + } + } + } + } + }); + + // Load initial data + const labels = []; + const data = []; + + // Get data from PHP-rendered elements with special data attributes + const dataElements = document.querySelectorAll('[data-category-name]'); + dataElements.forEach(element => { + labels.push(element.getAttribute('data-category-name')); + data.push(parseInt(element.getAttribute('data-category-amount'))); + }); + + // If no data elements found, try to parse from the script + if (labels.length === 0) { + try { + // This assumes there's PHP-rendered data in the original script + const categoryData = JSON.parse(document.getElementById('category-data').textContent); + categoryData.forEach(item => { + labels.push(item.name); + data.push(item.amount); + }); + } catch (e) { + console.warn('Could not load value chart data', e); + } + } + + // Update chart with data + window.valueChart.data.labels = labels; + window.valueChart.data.datasets[0].data = data; + window.valueChart.update(); +} + +// Refresh chart data from server +function refreshCharts() { + // Try to update charts with data present in the page first + try { + initCategoryChart(); + initValueChart(); + } catch (e) { + console.warn('Could not refresh charts with page data', e); + } +} \ No newline at end of file diff --git a/views/reporttt/report.php b/views/reporttt/report.php new file mode 100644 index 0000000..bf82167 --- /dev/null +++ b/views/reporttt/report.php @@ -0,0 +1,1333 @@ +prepare($sql); + + if (!empty($params)) { + $stmt->bind_param($types, ...$params); + } + + $stmt->execute(); + $result = $stmt->get_result(); + + $reports = []; + while ($row = $result->fetch_assoc()) { + $reports[] = $row; + } + + $stmt->close(); + + return $reports; +} + +// Function to get all products for filter dropdown +function getAllProducts($conn) { + $sql = "SELECT id_barang, nama_barang FROM barang ORDER BY nama_barang"; + $result = $conn->query($sql); + + $products = []; + if ($result->num_rows > 0) { + while ($row = $result->fetch_assoc()) { + $products[] = $row; + } + } + + return $products; +} + +// Function to get all categories for filter dropdown +function getAllCategories($conn) { + $sql = "SELECT id_jenis, nama_jenis FROM jenis_barang ORDER BY nama_jenis"; + $result = $conn->query($sql); + + $categories = []; + if ($result->num_rows > 0) { + while ($row = $result->fetch_assoc()) { + $categories[] = $row; + } + } + + return $categories; +} + +// Function to calculate summary statistics +function calculateSummary($reports) { + $summary = [ + 'total_items' => 0, + 'total_amount' => 0, + 'total_reports' => count($reports), + 'categories' => [] + ]; + + $category_totals = []; + + foreach ($reports as $report) { + $summary['total_items'] += $report['jumlah']; + $total_value = $report['jumlah'] * $report['harga']; + $summary['total_amount'] += $total_value; + + // Track by category + $category = $report['nama_jenis']; + if (!isset($category_totals[$category])) { + $category_totals[$category] = [ + 'count' => 0, + 'amount' => 0 + ]; + } + + $category_totals[$category]['count'] += $report['jumlah']; + $category_totals[$category]['amount'] += $total_value; + } + + // Format category data for charts + foreach ($category_totals as $category => $data) { + $summary['categories'][] = [ + 'name' => $category, + 'count' => $data['count'], + 'amount' => $data['amount'] + ]; + } + + return $summary; +} + +// Check connection +if ($conn->connect_error) { + die("Connection failed: " . $conn->connect_error); +} + +// Get data based on filters +$reports = getReportData($conn, $start_date, $end_date, $filter_type, $filter_month, $filter_year, $product_id, $category_id); +$products = getAllProducts($conn); +$categories = getAllCategories($conn); +$summary = calculateSummary($reports); + +// Close the database connection +$conn->close(); + +// Format numbers for display +function formatCurrency($number) { + return 'Rp ' . number_format($number, 0, ',', '.'); +} +?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ + + + + + + + + + + + + +
+ + + +
+
+ + + +
+
+
+
+ +
+
+
+
Total Laporan
+
+
+
+ +
+
+
+ +
+
+
+
Total Items
+
+
+
+ +
+
+
+ +
+
+
+
Total Nilai
+
+
+
+ +
+
+
+ +
+
+
+
Kategori
+
+
+
+
+ + +
+
+
Filter Laporan
+
+
+ +
+
+ + +
+
+ + +
> +
+ +
+ + + + +
+
+
+ + +
> +
+ + +
+
+ + +
> +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+
+ + +
+
+
+
+
Distribusi Kategori
+
+
+
+ +
+
+
+
+
+
+
+
Nilai Per Kategori
+
+
+
+ +
+
+
+
+
+ + +
+
+
+

Data Laporan

+
+
+ + +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDTanggalProdukKategoriJumlahHarga SatuanTotalNota
Tidak ada data laporan untuk periode ini.
+
+ + +
+
+ + + + +
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/views/reporttt/report_export.php b/views/reporttt/report_export.php new file mode 100644 index 0000000..f0a4e78 --- /dev/null +++ b/views/reporttt/report_export.php @@ -0,0 +1,470 @@ +SetCreator('Ayula Store'); + $pdf->SetAuthor('Ayula Store'); + $pdf->SetTitle('Laporan Inventaris'); + $pdf->SetSubject('Laporan Inventaris'); + $pdf->SetKeywords('Laporan, Inventaris, Ayula Store'); + + // Set default header data + $pdf->SetHeaderData('logo.png', 30, 'Ayula Store', 'Laporan Inventaris', array(0,64,255), array(0,64,128)); + $pdf->setFooterData(array(0,64,0), array(0,64,128)); + + // Set header and footer fonts + $pdf->setHeaderFont(Array(PDF_FONT_NAME_MAIN, '', PDF_FONT_SIZE_MAIN)); + $pdf->setFooterFont(Array(PDF_FONT_NAME_DATA, '', PDF_FONT_SIZE_DATA)); + + // Set default monospaced font + $pdf->SetDefaultMonospacedFont(PDF_FONT_MONOSPACED); + + // Set margins + $pdf->SetMargins(PDF_MARGIN_LEFT, PDF_MARGIN_TOP, PDF_MARGIN_RIGHT); + $pdf->SetHeaderMargin(PDF_MARGIN_HEADER); + $pdf->SetFooterMargin(PDF_MARGIN_FOOTER); + + // Set auto page breaks + $pdf->SetAutoPageBreak(TRUE, PDF_MARGIN_BOTTOM); + + // Set image scale factor + $pdf->setImageScale(PDF_IMAGE_SCALE_RATIO); + + // Add a page + $pdf->AddPage(); + + // Set font + $pdf->SetFont('helvetica', '', 10); + + // Get report data + $conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME); + if ($conn->connect_error) { + die("Connection failed: " . $conn->connect_error); + } + + // Build query based on filters + $sql = "SELECT r.id_report, r.tanggal, r.jumlah, r.harga, r.image, + b.nama_barang, b.kode_barang, + jb.nama_jenis + FROM report r + JOIN barang b ON r.id_barang = b.id_barang + JOIN jenis_barang jb ON b.id_jenis = jb.id_jenis + WHERE 1=1"; + + $params = array(); + $types = ""; + + if (!empty($filters['start_date']) && !empty($filters['end_date'])) { + $sql .= " AND DATE(r.tanggal) BETWEEN ? AND ?"; + $params[] = $filters['start_date']; + $params[] = $filters['end_date']; + $types .= "ss"; + } elseif (!empty($filters['month']) && !empty($filters['year'])) { + $sql .= " AND MONTH(r.tanggal) = ? AND YEAR(r.tanggal) = ?"; + $params[] = $filters['month']; + $params[] = $filters['year']; + $types .= "ss"; + } elseif (!empty($filters['year'])) { + $sql .= " AND YEAR(r.tanggal) = ?"; + $params[] = $filters['year']; + $types .= "s"; + } + + if (!empty($filters['product_id'])) { + $sql .= " AND b.id_barang = ?"; + $params[] = $filters['product_id']; + $types .= "i"; + } + + if (!empty($filters['category_id'])) { + $sql .= " AND b.id_jenis = ?"; + $params[] = $filters['category_id']; + $types .= "i"; + } + + $sql .= " ORDER BY r.tanggal DESC"; + + $stmt = $conn->prepare($sql); + + if (!empty($params)) { + $stmt->bind_param($types, ...$params); + } + + $stmt->execute(); + $result = $stmt->get_result(); + + // Add report title + $title = 'Laporan Inventaris Ayula Store'; + $period = ''; + + if (!empty($filters['start_date']) && !empty($filters['end_date'])) { + $start = date('d M Y', strtotime($filters['start_date'])); + $end = date('d M Y', strtotime($filters['end_date'])); + $period = "Periode: $start - $end"; + } elseif (!empty($filters['month']) && !empty($filters['year'])) { + $month = date('F', mktime(0, 0, 0, $filters['month'], 1)); + $period = "Periode: $month {$filters['year']}"; + } elseif (!empty($filters['year'])) { + $period = "Periode: Tahun {$filters['year']}"; + } + + // Add title and period + $pdf->SetFont('helvetica', 'B', 15); + $pdf->Cell(0, 10, $title, 0, 1, 'C'); + $pdf->SetFont('helvetica', '', 12); + $pdf->Cell(0, 10, $period, 0, 1, 'C'); + $pdf->Ln(5); + + // Add summary information + $pdf->SetFont('helvetica', 'B', 12); + $pdf->Cell(0, 10, 'Ringkasan', 0, 1, 'L'); + $pdf->SetFont('helvetica', '', 10); + + $total_items = 0; + $total_amount = 0; + $reports_count = $result->num_rows; + + // Create table headers + $pdf->SetFont('helvetica', 'B', 9); + $pdf->SetFillColor(230, 230, 230); + $pdf->Cell(15, 7, 'ID', 1, 0, 'C', 1); + $pdf->Cell(25, 7, 'Tanggal', 1, 0, 'C', 1); + $pdf->Cell(50, 7, 'Produk', 1, 0, 'C', 1); + $pdf->Cell(30, 7, 'Kategori', 1, 0, 'C', 1); + $pdf->Cell(15, 7, 'Jumlah', 1, 0, 'C', 1); + $pdf->Cell(25, 7, 'Harga', 1, 0, 'C', 1); + $pdf->Cell(30, 7, 'Total', 1, 1, 'C', 1); + + // Add data rows + $pdf->SetFont('helvetica', '', 8); + + while ($row = $result->fetch_assoc()) { + $total = $row['jumlah'] * $row['harga']; + $total_items += $row['jumlah']; + $total_amount += $total; + + $pdf->Cell(15, 6, $row['id_report'], 1, 0, 'C'); + $pdf->Cell(25, 6, date('d M Y', strtotime($row['tanggal'])), 1, 0, 'C'); + $pdf->Cell(50, 6, $row['nama_barang'], 1, 0, 'L'); + $pdf->Cell(30, 6, $row['nama_jenis'], 1, 0, 'L'); + $pdf->Cell(15, 6, $row['jumlah'], 1, 0, 'C'); + $pdf->Cell(25, 6, 'Rp ' . number_format($row['harga'], 0, ',', '.'), 1, 0, 'R'); + $pdf->Cell(30, 6, 'Rp ' . number_format($total, 0, ',', '.'), 1, 1, 'R'); + } + + // Add totals row + $pdf->SetFont('helvetica', 'B', 9); + $pdf->Cell(120, 7, 'TOTAL', 1, 0, 'R', 1); + $pdf->Cell(15, 7, $total_items, 1, 0, 'C', 1); + $pdf->Cell(25, 7, '', 1, 0, 'C', 1); + $pdf->Cell(30, 7, 'Rp ' . number_format($total_amount, 0, ',', '.'), 1, 1, 'R', 1); + + // Add summary before table + $pdf->SetY(60); // Position at beginning + $pdf->SetFont('helvetica', '', 10); + $pdf->Cell(60, 7, 'Total Laporan: ' . $reports_count, 0, 1, 'L'); + $pdf->Cell(60, 7, 'Total Items: ' . $total_items, 0, 1, 'L'); + $pdf->Cell(60, 7, 'Total Nilai: Rp ' . number_format($total_amount, 0, ',', '.'), 0, 1, 'L'); + $pdf->Ln(5); + + // Close and output PDF + $pdf->Output('Laporan_Inventaris_' . date('Y-m-d') . '.pdf', 'I'); + + $stmt->close(); + $conn->close(); +} + +// Function to export to Excel +function exportExcel($filters) { + require_once '../vendor/autoload.php'; // Require PhpSpreadsheet + + use PhpOffice\PhpSpreadsheet\Spreadsheet; + use PhpOffice\PhpSpreadsheet\Writer\Xlsx; + + // Create new Spreadsheet object + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + // Set document properties + $spreadsheet->getProperties() + ->setCreator('Ayula Store') + ->setLastModifiedBy('Ayula Store') + ->setTitle('Laporan Inventaris') + ->setSubject('Laporan Inventaris Ayula Store') + ->setDescription('Laporan Inventaris Ayula Store'); + + // Get report data + $conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME); + if ($conn->connect_error) { + die("Connection failed: " . $conn->connect_error); + } + + // Build query based on filters + $sql = "SELECT r.id_report, r.tanggal, r.jumlah, r.harga, r.image, + b.nama_barang, b.kode_barang, + jb.nama_jenis + FROM report r + JOIN barang b ON r.id_barang = b.id_barang + JOIN jenis_barang jb ON b.id_jenis = jb.id_jenis + WHERE 1=1"; + + $params = array(); + $types = ""; + + if (!empty($filters['start_date']) && !empty($filters['end_date'])) { + $sql .= " AND DATE(r.tanggal) BETWEEN ? AND ?"; + $params[] = $filters['start_date']; + $params[] = $filters['end_date']; + $types .= "ss"; + } elseif (!empty($filters['month']) && !empty($filters['year'])) { + $sql .= " AND MONTH(r.tanggal) = ? AND YEAR(r.tanggal) = ?"; + $params[] = $filters['month']; + $params[] = $filters['year']; + $types .= "ss"; + } elseif (!empty($filters['year'])) { + $sql .= " AND YEAR(r.tanggal) = ?"; + $params[] = $filters['year']; + $types .= "s"; + } + + if (!empty($filters['product_id'])) { + $sql .= " AND b.id_barang = ?"; + $params[] = $filters['product_id']; + $types .= "i"; + } + + if (!empty($filters['category_id'])) { + $sql .= " AND b.id_jenis = ?"; + $params[] = $filters['category_id']; + $types .= "i"; + } + + $sql .= " ORDER BY r.tanggal DESC"; + + $stmt = $conn->prepare($sql); + + if (!empty($params)) { + $stmt->bind_param($types, ...$params); + } + + $stmt->execute(); + $result = $stmt->get_result(); + + // Add title + $sheet->setCellValue('A1', 'LAPORAN INVENTARIS AYULA STORE'); + $sheet->mergeCells('A1:G1'); + $sheet->getStyle('A1')->getFont()->setBold(true)->setSize(14); + $sheet->getStyle('A1')->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER); + + // Add period + $period = ''; + if (!empty($filters['start_date']) && !empty($filters['end_date'])) { + $start = date('d M Y', strtotime($filters['start_date'])); + $end = date('d M Y', strtotime($filters['end_date'])); + $period = "Periode: $start - $end"; + } elseif (!empty($filters['month']) && !empty($filters['year'])) { + $month = date('F', mktime(0, 0, 0, $filters['month'], 1)); + $period = "Periode: $month {$filters['year']}"; + } elseif (!empty($filters['year'])) { + $period = "Periode: Tahun {$filters['year']}"; + } + + $sheet->setCellValue('A2', $period); + $sheet->mergeCells('A2:G2'); + $sheet->getStyle('A2')->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER); + + // Add summary information + $total_items = 0; + $total_amount = 0; + $reports_count = $result->num_rows; + + // Calculate totals + $data = []; + while ($row = $result->fetch_assoc()) { + $data[] = $row; + $total_items += $row['jumlah']; + $total_amount += ($row['jumlah'] * $row['harga']); + } + + // Add summary section + $sheet->setCellValue('A4', 'Ringkasan:'); + $sheet->getStyle('A4')->getFont()->setBold(true); + + $sheet->setCellValue('A5', 'Total Laporan:'); + $sheet->setCellValue('B5', $reports_count); + + $sheet->setCellValue('A6', 'Total Items:'); + $sheet->setCellValue('B6', $total_items); + + $sheet->setCellValue('A7', 'Total Nilai:'); + $sheet->setCellValue('B7', 'Rp ' . number_format($total_amount, 0, ',', '.')); + + // Add table headers + $sheet->setCellValue('A9', 'ID'); + $sheet->setCellValue('B9', 'Tanggal'); + $sheet->setCellValue('C9', 'Produk'); + $sheet->setCellValue('D9', 'Kategori'); + $sheet->setCellValue('E9', 'Jumlah'); + $sheet->setCellValue('F9', 'Harga'); + $sheet->setCellValue('G9', 'Total'); + + // Style the headers + $sheet->getStyle('A9:G9')->getFont()->setBold(true); + $sheet->getStyle('A9:G9')->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)->getStartColor()->setRGB('CCCCCC'); + $sheet->getStyle('A9:G9')->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER); + + // Add data rows + $row_num = 10; + foreach ($data as $row) { + $total = $row['jumlah'] * $row['harga']; + + $sheet->setCellValue('A' . $row_num, $row['id_report']); + $sheet->setCellValue('B' . $row_num, date('d M Y', strtotime($row['tanggal']))); + $sheet->setCellValue('C' . $row_num, $row['nama_barang']); + $sheet->setCellValue('D' . $row_num, $row['nama_jenis']); + $sheet->setCellValue('E' . $row_num, $row['jumlah']); + $sheet->setCellValue('F' . $row_num, 'Rp ' . number_format($row['harga'], 0, ',', '.')); + $sheet->setCellValue('G' . $row_num, 'Rp ' . number_format($total, 0, ',', '.')); + + $row_num++; + } + + // Add totals row + $sheet->setCellValue('A' . $row_num, 'TOTAL'); + $sheet->mergeCells('A' . $row_num . ':D' . $row_num); + $sheet->setCellValue('E' . $row_num, $total_items); + $sheet->setCellValue('G' . $row_num, 'Rp ' . number_format($total_amount, 0, ',', '.')); + + $sheet->getStyle('A' . $row_num . ':G' . $row_num)->getFont()->setBold(true); + $sheet->getStyle('A' . $row_num . ':G' . $row_num)->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)->getStartColor()->setRGB('EEEEEE'); + + // Autosize columns + foreach(range('A', 'G') as $col) { + $sheet->getColumnDimension($col)->setAutoSize(true); + } + + // Create a border for the table + $styleArray = [ + 'borders' => [ + 'allBorders' => [ + 'borderStyle' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN, + ], + ], + ]; + $sheet->getStyle('A9:G' . $row_num)->applyFromArray($styleArray); + + // Output excel file + header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + header('Content-Disposition: attachment;filename="Laporan_Inventaris_' . date('Y-m-d') . '.xlsx"'); + header('Cache-Control: max-age=0'); + + $writer = new Xlsx($spreadsheet); + $writer->save('php://output'); + + $stmt->close(); + $conn->close(); + exit(); +} + +// Process report export request +if (isset($_GET['export']) && in_array($_GET['export'], ['pdf', 'excel'])) { + $filters = [ + 'start_date' => isset($_GET['start_date']) ? $_GET['start_date'] : '', + 'end_date' => isset($_GET['end_date']) ? $_GET['end_date'] : '', + 'month' => isset($_GET['filter_month']) ? $_GET['filter_month'] : '', + 'year' => isset($_GET['filter_year']) ? $_GET['filter_year'] : '', + 'product_id' => isset($_GET['product_id']) ? $_GET['product_id'] : '', + 'category_id' => isset($_GET['category_id']) ? $_GET['category_id'] : '' + ]; + + if ($_GET['export'] == 'pdf') { + generatePDF($filters); + } else { + exportExcel($filters); + } + exit(); +} + +// Handle AJAX request for chart data +if (isset($_GET['action']) && $_GET['action'] == 'get_chart_data') { + $conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME); + if ($conn->connect_error) { + die(json_encode(['error' => 'Connection failed: ' . $conn->connect_error])); + } + + // Build query based on filters + $sql = "SELECT jb.nama_jenis, SUM(r.jumlah) as total_items, SUM(r.jumlah * r.harga) as total_value + FROM report r + JOIN barang b ON r.id_barang = b.id_barang + JOIN jenis_barang jb ON b.id_jenis = jb.id_jenis + WHERE 1=1"; + + $params = array(); + $types = ""; + + if (!empty($_GET['start_date']) && !empty($_GET['end_date'])) { + $sql .= " AND DATE(r.tanggal) BETWEEN ? AND ?"; + $params[] = $_GET['start_date']; + $params[] = $_GET['end_date']; + $types .= "ss"; + } elseif (!empty($_GET['filter_month']) && !empty($_GET['filter_year'])) { + $sql .= " AND MONTH(r.tanggal) = ? AND YEAR(r.tanggal) = ?"; + $params[] = $_GET['filter_month']; + $params[] = $_GET['filter_year']; + $types .= "ss"; + } elseif (!empty($_GET['filter_year'])) { + $sql .= " AND YEAR(r.tanggal) = ?"; + $params[] = $_GET['filter_year']; + $types .= "s"; + } + + if (!empty($_GET['product_id'])) { + $sql .= " AND b.id_barang = ?"; + $params[] = $_GET['product_id']; + $types .= "i"; + } + + if (!empty($_GET['category_id'])) { + $sql .= " AND b.id_jenis = ?"; + $params[] = $_GET['category_id']; + $types .= "i"; + } + + $sql .= " GROUP BY jb.nama_jenis"; + + $stmt = $conn->prepare($sql); + + if (!empty($params)) { + $stmt->bind_param($types, ...$params); + } + + $stmt->execute(); + $result = $stmt->get_result(); + + $chart_data = [ + 'labels' => [], + 'item_counts' => [], + 'values' => [] + ]; + + while ($row = $result->fetch_assoc()) { + $chart_data['labels'][] = $row['nama_jenis']; + $chart_data['item_counts'][] = (int)$row['total_items']; + $chart_data['values'][] = (float)$row['total_value']; + } + + echo json_encode($chart_data); + + $stmt->close(); + $conn->close(); + exit(); +} \ No newline at end of file diff --git a/views/transaction/barcode-scanner.css b/views/transaction/barcode-scanner.css new file mode 100644 index 0000000..cca5d5b --- /dev/null +++ b/views/transaction/barcode-scanner.css @@ -0,0 +1,160 @@ +/* Barcode Scanner Styles */ +.barcode-scanner-container { + position: relative; + z-index: 100; + background: linear-gradient(45deg, #ff9f4380, #ff7f5080); + border-radius: 10px; + padding: 15px; + margin-bottom: 20px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; +} + +.barcode-scanner-container.sticky-scanner { + position: fixed; + left: 0; + right: 0; + margin: 0 auto; + width: calc(100% - 30px); + max-width: 1140px; + border-radius: 0 0 10px 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + padding: 10px 15px; + z-index: 1000; +} + +.barcode-scanner-container.collapsed { + padding: 5px 15px; +} + +.barcode-scanner-container.collapsed .barcode-content { + display: none; +} + +.barcode-scanner-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + cursor: pointer; +} + +.barcode-scanner-container.collapsed .barcode-scanner-header { + margin-bottom: 0; +} + +.barcode-scanner-header h5 { + margin: 0; + color: #fff; + font-weight: 600; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1); +} + +.barcode-toggle-btn { + background: rgba(255, 255, 255, 0.3); + color: #fff; + border: none; + border-radius: 5px; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.barcode-content { + position: relative; +} + +.barcode-input-group { + display: flex; + align-items: center; + background: #fff; + border-radius: 5px; + overflow: hidden; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.barcode-input-group .input-group-text { + background-color: #f8f9fa; + border: none; + padding: 10px 15px; +} + +#barcode-input { + border: none; + box-shadow: none; + padding: 10px 15px; + font-size: 16px; + background-color: #fff; + flex-grow: 1; +} + +#barcode-input:focus { + outline: none; +} + +#barcode-status { + margin-top: 8px; + font-size: 14px; + color: #6c757d; + height: 20px; +} + +/* Scanning animation */ +.barcode-scanner-container.scanning { + background: linear-gradient(45deg, #ff9f4330, #ff7f5030); +} + +.barcode-scanner-container.scanning .barcode-input-group { + box-shadow: 0 0 0 2px #ff9f43; +} + +/* Highlight product in grid */ +.productset.highlight-product { + border: 2px solid #28a745; + box-shadow: 0 0 15px rgba(40, 167, 69, 0.5); + animation: highlight-pulse 1.5s; +} + +@keyframes highlight-pulse { + 0% { + transform: scale(1); + box-shadow: 0 0 0 rgba(40, 167, 69, 0.5); + } + 50% { + transform: scale(1.05); + box-shadow: 0 0 15px rgba(40, 167, 69, 0.8); + } + 100% { + transform: scale(1); + box-shadow: 0 0 0 rgba(40, 167, 69, 0.5); + } +} + +/* Product Added Notification */ +.barcode-notification { + position: fixed; + top: 70px; + right: 20px; + background-color: #28a745; + color: white; + padding: 10px 15px; + border-radius: 5px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + z-index: 9999; + opacity: 0; + transform: translateX(50px); + transition: opacity 0.3s, transform 0.3s; + font-weight: bold; + max-width: 280px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.barcode-notification.show { + opacity: 1; + transform: translateX(0); +} \ No newline at end of file diff --git a/views/transaction/barcode-scanner.js b/views/transaction/barcode-scanner.js new file mode 100644 index 0000000..a9e545d --- /dev/null +++ b/views/transaction/barcode-scanner.js @@ -0,0 +1,453 @@ +/** + * Barcode Scanner Feature + * Adds barcode scanning functionality to Ayula Store POS System + */ + +$(document).ready(function() { + // Initialize barcode scanner functionality + initBarcodeScanner(); + + // Check for barcode scanner toggle + setupBarcodeScannerToggle(); + + // Force update the clear cart button state + forceUpdateClearCartButton(); +}); + +/** + * Force update the clear cart button state based on actual cart content + */ +function forceUpdateClearCartButton() { + // Check if cart actually has items + const hasItems = $('.product-lists').length > 0; + console.log('Force updating clear cart button. Cart has items:', hasItems); + + const clearCartBtn = $('#clear-cart-btn'); + + if (hasItems) { + clearCartBtn.attr('href', 'javascript:void(0);') + .removeClass('disabled') + .css({ + 'opacity': '1', + 'cursor': 'pointer' + }) + .off('click') + .on('click', function(e) { + e.preventDefault(); + if (typeof bootstrap !== 'undefined' && $('#clearCartModal').length) { + try { + const clearModal = new bootstrap.Modal(document.getElementById('clearCartModal')); + clearModal.show(); + } catch (error) { + console.log('Modal error, using fallback:', error); + if (confirm('Apakah Anda yakin ingin menghapus semua item dari keranjang Anda?')) { + window.location.href = 'index.php?clear_cart=1'; + } + } + } else { + if (confirm('Apakah Anda yakin ingin menghapus semua item dari keranjang Anda?')) { + window.location.href = 'index.php?clear_cart=1'; + } + } + }); + } else { + clearCartBtn.attr('href', '#') + .addClass('disabled') + .css({ + 'opacity': '0.5', + 'cursor': 'not-allowed' + }) + .off('click'); + } +} + +/** + * Initialize barcode scanner functionality + */ +function initBarcodeScanner() { + const barcodeInput = $('#barcode-input'); + + // Focus barcode input when barcode scanner container is clicked + $('.barcode-scanner-container').on('click', function(e) { + // Don't focus if clicking on a button or another interactive element + if (!$(e.target).is('button, a, input')) { + barcodeInput.focus(); + } + }); + + // Make barcode scanner sticky at top + makeBarcodeSticky(); + + // Handle barcode input + barcodeInput.on('keydown', function(e) { + // If Enter key is pressed, process the barcode + if (e.keyCode === 13) { + e.preventDefault(); + + const barcode = $(this).val().trim(); + if (barcode) { + processBarcode(barcode); + $(this).val(''); // Clear input after processing + } + } + }); + + // Auto focus the barcode input when page loads + setTimeout(function() { + barcodeInput.focus(); + }, 500); + + // Handle window focus to automatically focus barcode input + $(window).on('focus', function() { + // Only focus if scanner is not collapsed + if (!$('.barcode-scanner-container').hasClass('collapsed')) { + setTimeout(function() { + barcodeInput.focus(); + }, 100); + } + }); +} + +/** + * Make barcode scanner sticky at top of page + */ +function makeBarcodeSticky() { + const barcodeContainer = $('.barcode-scanner-container'); + if (!barcodeContainer.length) return; + + const originalPosition = barcodeContainer.offset() ? barcodeContainer.offset().top : 0; + const headerHeight = $('.header').outerHeight() || 0; + + $(window).on('scroll', function() { + const scrollPosition = $(window).scrollTop(); + + if (scrollPosition > originalPosition - headerHeight) { + barcodeContainer.addClass('sticky-scanner'); + barcodeContainer.css('top', headerHeight + 'px'); + } else { + barcodeContainer.removeClass('sticky-scanner'); + barcodeContainer.css('top', ''); + } + }); +} + +/** + * Setup toggle for barcode scanner container + */ +function setupBarcodeScannerToggle() { + $('#toggle-barcode-scanner').on('click', function() { + $('.barcode-scanner-container').toggleClass('collapsed'); + $(this).find('i').toggleClass('fa-chevron-up fa-chevron-down'); + + // Focus the input when expanded + if (!$('.barcode-scanner-container').hasClass('collapsed')) { + $('#barcode-input').focus(); + } + }); +} + +/** + * Process barcode and add product to cart + */ +function processBarcode(barcode) { + // Show loading indicator + $('.barcode-scanner-container').addClass('scanning'); + $('#barcode-status').html(' Memindai...'); + + // Keep focus on the barcode input field + const barcodeInput = $('#barcode-input'); + + // Send AJAX request to get product by barcode + $.ajax({ + url: 'get_product_by_barcode.php', + type: 'POST', + data: { + barcode: barcode + }, + dataType: 'json', + success: function(response) { + $('.barcode-scanner-container').removeClass('scanning'); + + if (response.success) { + // Show success message + $('#barcode-status').html( + '
Ditambahkan: ' + + response.product.name + '
' + ); + + // Highlight the product in the grid if visible (without scrolling) + highlightProductInGrid(response.product.id); + + // Update cart section via AJAX + if (response.cart_html) { + // Update the cart HTML + $('.product-table').html(response.cart_html); + + // Update cart totals and related UI elements + updateAllCartElements(response.cart_totals); + + // Make sure quantity controls and other event handlers are properly set up + if (typeof setupQuantityControls === 'function') { + setupQuantityControls(); + } + if (typeof setupCartDeletionConfirmation === 'function') { + setupCartDeletionConfirmation(); + } + + // Force update the clear cart button + forceUpdateClearCartButton(); + + // Trigger custom event for other scripts to respond to cart updates + document.dispatchEvent(new CustomEvent('cartUpdated')); + } + } else { + // Show error message + $('#barcode-status').html( + '
' + + response.message + '
' + ); + } + + // Reset status message after delay + setTimeout(function() { + $('#barcode-status').html(' Pindai barcode'); + // Restore focus to barcode input + barcodeInput.focus(); + // Try to restore cursor position + if (barcodeInput.get(0) && barcodeInput.get(0).setSelectionRange) { + barcodeInput.get(0).setSelectionRange(0, 0); + } + }, 3000); + }, + error: function(xhr, status, error) { + $('.barcode-scanner-container').removeClass('scanning'); + console.error('AJAX Error:', status, error); + + // Try to get more detailed error info + let errorMessage = 'Koneksi error'; + try { + if (xhr.responseText) { + // Check if it's JSON + try { + const errorData = JSON.parse(xhr.responseText); + if (errorData && errorData.message) { + errorMessage = errorData.message; + } + } catch (jsonError) { + // Not valid JSON, just use generic message + errorMessage = 'Kesalahan sistem. Silakan coba lagi.'; + console.log('Response was not valid JSON:', xhr.responseText.substring(0, 100)); + } + } + } catch (e) { + console.error('Error parsing error response:', e); + } + + $('#barcode-status').html( + '
' + errorMessage + '
' + ); + + // Reset status message after delay + setTimeout(function() { + $('#barcode-status').html(' Pindai barcode'); + barcodeInput.focus(); + }, 3000); + } + }); +} + +/** + * Comprehensive update of all cart-related elements + */ +function updateAllCartElements(totals) { + console.log('Updating all cart elements with totals:', totals); + + // 1. Update displayed values + $('.totalitem h4').text('Total barang: ' + totals.items); + $('.total-value h6').text('Rp. ' + totals.formatted_total); + $('.btn-totallabel h6').text('Rp. ' + totals.formatted_total); + + // 2. Update form inputs + $('#cash-amount').val(totals.formatted_total); + $('#hidden-cash-amount').val(totals.total); + + // 3. Create and replace checkout form if doesn't exist or needs updating + if (totals.items > 0) { + // Only replace if needed + if ($('#checkout-form').length === 0 || $('#checkout-form button[type="submit"]').prop('disabled')) { + const checkoutFormHtml = ` +
+ + + + +
+ `; + + // Replace current form with new one + if ($('#checkout-form').length > 0) { + $('#checkout-form').replaceWith(checkoutFormHtml); + } else { + // If form doesn't exist, replace the disabled button + $('.btn-totallabel.w-100[disabled]').replaceWith(checkoutFormHtml); + } + + // Setup the new form + if (typeof setupCheckoutForm === 'function') { + setupCheckoutForm(); + } + } else { + // Just enable the existing button + $('.btn-totallabel').prop('disabled', false); + $('#checkout-form button[type="submit"]').prop('disabled', false); + } + } else { + // Cart is empty, show disabled button + if ($('#checkout-form').length > 0) { + $('#checkout-form').replaceWith(` + + `); + } + } + + // 4. Update quick cash buttons + updateQuickCashButtons(totals.total); + + // 5. Show/hide change display + updateChangeDisplay(); + + // 6. Re-initialize scrollbars and event handlers + initializeCartScrolling(); +} + +/** + * Initialize scrolling behavior for cart + */ +function initializeCartScrolling() { + // Make sure product table has correct max height + const windowHeight = $(window).height(); + const headerHeight = $('.order-list').outerHeight() || 0; + const cardHeaderHeight = $('.card-order .card-body:first-of-type').outerHeight() || 0; + const cardFooterHeight = $('.card-order .card-body:last-of-type').outerHeight() || 0; + const availableHeight = windowHeight - headerHeight - cardHeaderHeight - cardFooterHeight - 50; // 50px buffer + + // Set max height for scrollable area + $('.product-table').css('max-height', availableHeight + 'px'); +} + +/** + * Highlight product in grid when added via barcode + */ +function highlightProductInGrid(productId) { + const productElement = $('.productset[data-product-id="' + productId + '"]'); + + if (productElement.length) { + // Flash highlight effect without scrolling + productElement.addClass('highlight-product'); + setTimeout(function() { + productElement.removeClass('highlight-product'); + }, 1500); + + // Optional: Briefly show which product was added via a popup notification + showAddedProductNotification(productElement.find('.productsetcontent h4').text()); + } +} + +/** + * Show a notification that a product was added instead of scrolling + */ +function showAddedProductNotification(productName) { + // Create notification element if it doesn't exist + if ($('#barcode-notification').length === 0) { + $('body').append('
'); + } + + // Show notification + $('#barcode-notification') + .text('Ditambahkan: ' + productName) + .addClass('show'); + + // Hide notification after 2 seconds + setTimeout(function() { + $('#barcode-notification').removeClass('show'); + }, 2000); +} + +/** + * Update quick cash buttons based on total amount + */ +function updateQuickCashButtons(totalAmount) { + if (totalAmount > 0) { + // Update round-up buttons for different denominations + const round1000 = Math.ceil(totalAmount / 1000) * 1000; + const round10000 = Math.ceil(totalAmount / 10000) * 10000; + const round50000 = Math.ceil(totalAmount / 50000) * 50000; + const round100000 = Math.ceil(totalAmount / 100000) * 100000; + + // Find quick cash buttons and update their values + $('.quick-cash').each(function(index) { + let newValue; + switch(index) { + case 0: newValue = round1000; break; + case 1: newValue = round10000; break; + case 2: newValue = round50000; break; + case 3: newValue = round100000; break; + default: newValue = totalAmount; + } + + $(this).data('value', newValue); + $(this).text('Rp. ' + formatNumber(newValue)); + }); + + // Show row containing quick cash buttons + $('.setvaluecash .row').show(); + } else { + // Hide quick cash buttons if cart is empty + $('.setvaluecash .row').hide(); + } +} + +/** + * Update change display based on current cash amount and total + */ +function updateChangeDisplay() { + const cashInput = $('#cash-amount'); + const changeDisplay = $('#change-amount'); + const changeContainer = $('#change-container'); + const hiddenCashAmount = $('#hidden-cash-amount'); + const hiddenChangeAmount = $('#hidden-change-amount'); + + // Get total from the displayed value or use 0 if not available + const totalText = $('.total-value h6').text(); + const totalAmount = parseFloat(totalText.replace(/[^\d]/g, '')) || 0; + + // Get cash value from hidden field + let cashValue = parseFloat(hiddenCashAmount.val()) || 0; + + // Calculate change + const change = cashValue - totalAmount; + + // Update hidden field for change amount + hiddenChangeAmount.val(Math.max(0, change)); + + // Format and display + if (change >= 0 && totalAmount > 0) { + changeDisplay.text('Rp. ' + formatNumber(change)); + changeContainer.show(); + } else { + changeContainer.hide(); + } +} + +/** + * Format number with thousand separator + */ +function formatNumber(number) { + return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, "."); +} \ No newline at end of file diff --git a/views/transaction/cart-functionality.js b/views/transaction/cart-functionality.js new file mode 100644 index 0000000..9215ddc --- /dev/null +++ b/views/transaction/cart-functionality.js @@ -0,0 +1,223 @@ +/** + * Ayula Store POS System - Cart Functionality + * Handles cart operations and user interactions for shopping cart + */ + +$(document).ready(function() { + // Setup quantity controls for cart items + setupQuantityControls(); + + // Setup checkout form functionality + setupCheckoutForm(); + + // Setup cart item deletion confirmation + setupCartDeletionConfirmation(); + + // Setup clear cart confirmation + setupClearCartConfirmation(); + + // Setup cash amount input formatting + setupCashAmountFormatting(); +}); + +/** + * Set up quantity controls (increment, decrement, manual input) for cart items + */ +function setupQuantityControls() { + // Handle increment button + $('.quantity-btn.increment').off('click').on('click', function() { + const input = $(this).siblings('.quantity-field'); + const currentValue = parseInt(input.val()) || 1; + const maxStock = parseInt(input.data('max-stock')) || 999; + + if (currentValue < maxStock) { + input.val(currentValue + 1); + } + }); + + // Handle decrement button + $('.quantity-btn.decrement').off('click').on('click', function() { + const input = $(this).siblings('.quantity-field'); + const currentValue = parseInt(input.val()) || 1; + + if (currentValue > 1) { + input.val(currentValue - 1); + } + }); + + // Validate manual input in quantity field + $('.quantity-field').off('change keyup').on('change keyup', function() { + let value = parseInt($(this).val()) || 1; + const maxStock = parseInt($(this).data('max-stock')) || 999; + + // Ensure value is within limits + if (value < 1) value = 1; + if (value > maxStock) value = maxStock; + + $(this).val(value); + }); +} + +/** + * Set up checkout form validation and submission + */ +function setupCheckoutForm() { + $('#checkout-form').off('submit').on('submit', function(e) { + // Get the cash amount and total + const cashAmount = parseFloat($('#hidden-cash-amount').val()) || 0; + const totalText = $('.total-value h6').text(); + const totalAmount = parseFloat(totalText.replace(/[^\d]/g, '')) || 0; + + // Validate that cash amount is sufficient + if (cashAmount < totalAmount) { + e.preventDefault(); + alert('Jumlah tunai tidak mencukupi. Mohon masukkan nilai yang sama atau lebih besar dari total.'); + return false; + } + + // All good, allow form submission + return true; + }); +} + +/** + * Setup confirmation dialog when removing items from cart + */ +function setupCartDeletionConfirmation() { + $('.delete-cart-item').off('click').on('click', function(e) { + e.preventDefault(); + const index = $(this).data('index'); + + // Set up the confirmation link to the correct index + $('#confirm-delete-btn').attr('href', 'index.php?remove_item=' + index); + + // Show modal if available, otherwise use confirm dialog + if (typeof bootstrap !== 'undefined' && $('#deleteConfirmModal').length) { + try { + const modal = new bootstrap.Modal(document.getElementById('deleteConfirmModal')); + modal.show(); + } catch (error) { + console.error('Modal error, using fallback:', error); + if (confirm('Apakah Anda yakin ingin menghapus item ini dari keranjang?')) { + window.location.href = 'index.php?remove_item=' + index; + } + } + } else { + if (confirm('Apakah Anda yakin ingin menghapus item ini dari keranjang?')) { + window.location.href = 'index.php?remove_item=' + index; + } + } + }); +} + +/** + * Setup clear cart confirmation + */ +function setupClearCartConfirmation() { + // This is handled in another part of the code (index.php inline script) + // Just making sure all confirmations are set up correctly + + // Clear Cart confirmation - Confirm button handler + $('#confirm-clear-cart').off('click').on('click', function(e) { + window.location.href = 'index.php?clear_cart=1'; + }); + + // Clear Cart confirmation - Cancel button handler + $('#cancel-clear-cart').off('click').on('click', function() { + if (typeof bootstrap !== 'undefined') { + try { + const clearModal = bootstrap.Modal.getInstance(document.getElementById('clearCartModal')); + if (clearModal) { + clearModal.hide(); + } + } catch (error) { + console.error('Modal hide error:', error); + // Force modal to close + $('#clearCartModal').modal('hide'); + } + } + }); +} + +/** + * Setup cash amount input formatting with thousand separator + */ +function setupCashAmountFormatting() { + const cashInput = $('#cash-amount'); + const hiddenCashAmount = $('#hidden-cash-amount'); + const hiddenChangeAmount = $('#hidden-change-amount'); + const changeDisplay = $('#change-amount'); + const changeContainer = $('#change-container'); + + // Format the input with thousand separator on change + cashInput.on('input', function() { + // Remove non-numeric characters + let value = $(this).val().replace(/[^\d]/g, ''); + + // Get the total amount + const totalText = $('.total-value h6').text(); + const totalAmount = parseFloat(totalText.replace(/[^\d]/g, '')) || 0; + + // Convert to number and format with thousand separator + if (value !== '') { + const numericValue = parseInt(value); + + // Calculate change + const change = numericValue - totalAmount; + + // Update hidden fields for form submission + hiddenCashAmount.val(numericValue); + hiddenChangeAmount.val(Math.max(0, change)); + + // Format display value + $(this).val(formatNumber(numericValue)); + + // Show change if payment is sufficient + if (change >= 0) { + changeDisplay.text('Rp. ' + formatNumber(change)); + changeContainer.show(); + } else { + changeContainer.hide(); + } + } else { + $(this).val(''); + changeContainer.hide(); + } + }); + + // Quick cash buttons + $('.quick-cash').on('click', function() { + const value = $(this).data('value'); + cashInput.val(formatNumber(value)); + + // Calculate change + const totalText = $('.total-value h6').text(); + const totalAmount = parseFloat(totalText.replace(/[^\d]/g, '')) || 0; + const change = value - totalAmount; + + // Update hidden fields for form submission + hiddenCashAmount.val(value); + hiddenChangeAmount.val(Math.max(0, change)); + + // Show change + if (change >= 0) { + changeDisplay.text('Rp. ' + formatNumber(change)); + changeContainer.show(); + } else { + changeContainer.hide(); + } + }); + + // Trigger input event to initialize on page load + cashInput.trigger('input'); +} + +/** + * Format number with thousand separator + * + * @param {number} number The number to format + * @return {string} Formatted number with thousand separator + */ +function formatNumber(number) { + return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, "."); +} \ No newline at end of file diff --git a/views/transaction/configtrans.php b/views/transaction/configtrans.php new file mode 100644 index 0000000..f957848 --- /dev/null +++ b/views/transaction/configtrans.php @@ -0,0 +1,484 @@ +connect_error) { + error_log("Database connection failed: " . ($conn ? $conn->connect_error : "Connection not established")); + die("Database connection failed. Please try again later."); +} + +// Set error reporting for debugging +error_reporting(E_ALL); +ini_set('display_errors', 1); +ini_set('log_errors', 1); +ini_set('error_log', '../../logs/php-errors.log'); + +// Check if user is logged in, redirect to login page if not +if (!isset($_SESSION['user_id']) || !isset($_SESSION['role'])) { + // Check if this is an AJAX request + if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') { + // Return JSON error for AJAX requests + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'message' => 'Session expired. Please login again.', 'redirect' => '/ayula-store/views/login/']); + exit; + } else { + // Redirect for regular requests + header('Location: /ayula-store/views/login/'); + exit; + } +} + +// Set variables for use throughout the application +$userRole = $_SESSION['role'] ?? ''; // 'user' or 'admin' +$username = $_SESSION['username'] ?? 'Unknown User'; +$isAdmin = ($userRole === 'admin'); + +/** + * Get all products or filter by product type and/or search query + * + * @param int|null $typeId Optional product type ID to filter by + * @param string|null $searchQuery Optional search term to filter product names + * @return array List of products + */ +function getProducts($typeId = null, $searchQuery = null) { + global $conn; + + try { + $sql = "SELECT b.*, j.nama_jenis + FROM barang_kasir b + JOIN jenis_barang j ON b.id_jenis = j.id_jenis + WHERE 1=1"; + + $params = []; + $types = ""; + + // Add type filter if specified + if ($typeId) { + $sql .= " AND b.id_jenis = ?"; + $params[] = $typeId; + $types .= "i"; // integer parameter + } + + // Add search filter if specified + if ($searchQuery && !empty($searchQuery)) { + $sql .= " AND (b.nama_barang LIKE ? OR b.kode_barang LIKE ?)"; + $params[] = "%" . $searchQuery . "%"; + $params[] = "%" . $searchQuery . "%"; + $types .= "ss"; // two string parameters + } + + $sql .= " ORDER BY b.nama_barang ASC"; + + $stmt = mysqli_prepare($conn, $sql); + + if (!$stmt) { + error_log("Query preparation failed: " . mysqli_error($conn)); + return []; + } + + // Bind parameters dynamically if we have any + if (!empty($params)) { + $bindParams = array(&$stmt, &$types); + foreach($params as $key => $value) { + $bindParams[] = &$params[$key]; + } + call_user_func_array('mysqli_stmt_bind_param', $bindParams); + } + + $executed = mysqli_stmt_execute($stmt); + + if (!$executed) { + error_log("Query execution failed: " . mysqli_stmt_error($stmt)); + mysqli_stmt_close($stmt); + return []; + } + + $result = mysqli_stmt_get_result($stmt); + + $products = []; + while ($row = mysqli_fetch_assoc($result)) { + $products[] = $row; + } + + mysqli_stmt_close($stmt); + return $products; + } catch (Exception $e) { + error_log("Exception in getProducts: " . $e->getMessage()); + return []; + } +} + +/** + * Get product details by ID + * + * @param int $productId The product ID + * @return array|null Product details or null if not found + */ +function getProductById($productId) { + global $conn; + + try { + $stmt = mysqli_prepare($conn, "SELECT b.*, j.nama_jenis + FROM barang_kasir b + LEFT JOIN jenis_barang j ON b.id_jenis = j.id_jenis + WHERE b.id_barangK = ?"); + + if (!$stmt) { + error_log("Query preparation failed: " . mysqli_error($conn)); + return null; + } + + mysqli_stmt_bind_param($stmt, "i", $productId); + $executed = mysqli_stmt_execute($stmt); + + if (!$executed) { + error_log("Query execution failed: " . mysqli_stmt_error($stmt)); + mysqli_stmt_close($stmt); + return null; + } + + $result = mysqli_stmt_get_result($stmt); + $product = mysqli_fetch_assoc($result); + mysqli_stmt_close($stmt); + + return $product; + } catch (Exception $e) { + error_log("Exception in getProductById: " . $e->getMessage()); + return null; + } +} + +/** + * Get all product types (jenis_barang) + * + * @return array List of product types + */ +function getProductTypes() { + global $conn; + + try { + $query = "SELECT * FROM jenis_barang ORDER BY nama_jenis"; + $result = mysqli_query($conn, $query); + + if (!$result) { + error_log("Query failed: " . mysqli_error($conn)); + return []; + } + + $types = []; + while ($row = mysqli_fetch_assoc($result)) { + $types[] = $row; + } + + return $types; + } catch (Exception $e) { + error_log("Exception in getProductTypes: " . $e->getMessage()); + return []; + } +} + +/** + * Create a new transaction + * + * @param array $items Array of items with product_id and quantity + * @param float $total Total amount + * @param float $cashAmount Cash amount received from customer + * @param float $changeAmount Change amount to be returned to customer + * @return array Result with success status and transaction ID + */ +function createTransaction($items, $total, $cashAmount = 0, $changeAmount = 0) { + global $conn; + + if (empty($items)) { + return [ + 'success' => false, + 'message' => 'No items in cart' + ]; + } + + $totalItems = array_sum(array_column($items, 'quantity')); + + // Generate transaction code (e.g., TRX-20250416-001) + $transactionCode = 'TRX-' . date('Ymd') . '-' . rand(100, 999); + + // Begin transaction + mysqli_begin_transaction($conn); + + try { + // Insert into transaksi table with cash_amount and change_amount + $stmt = mysqli_prepare($conn, "INSERT INTO transaksi (kode_transaksi, total_item, total, metode_pembayaran, cash_amount, change_amount) VALUES (?, ?, ?, 'Cash', ?, ?)"); + + if (!$stmt) { + throw new Exception("Failed to prepare transaction statement: " . mysqli_error($conn)); + } + + mysqli_stmt_bind_param($stmt, "siddd", $transactionCode, $totalItems, $total, $cashAmount, $changeAmount); + $executed = mysqli_stmt_execute($stmt); + + if (!$executed) { + throw new Exception("Failed to execute transaction statement: " . mysqli_stmt_error($stmt)); + } + + // Get the ID of the transaction just created + $transactionId = mysqli_insert_id($conn); + + if (!$transactionId) { + throw new Exception("Failed to get transaction ID"); + } + + // Insert each item into detail_transaksi + foreach ($items as $item) { + $product = getProductById($item['product_id']); + + if (!$product) { + throw new Exception("Product not found: ID=" . $item['product_id']); + } + + $itemTotal = $product['harga'] * $item['quantity']; + + $stmt = mysqli_prepare($conn, "INSERT INTO detail_transaksi (id_transaksi, id_barangK, jumlah, harga_satuan, total_harga) VALUES (?, ?, ?, ?, ?)"); + + if (!$stmt) { + throw new Exception("Failed to prepare detail statement: " . mysqli_error($conn)); + } + + mysqli_stmt_bind_param($stmt, "iiddd", $transactionId, $item['product_id'], $item['quantity'], $product['harga'], $itemTotal); + $executed = mysqli_stmt_execute($stmt); + + if (!$executed) { + throw new Exception("Failed to execute detail statement: " . mysqli_stmt_error($stmt)); + } + + // Update inventory (reduce stock) + $stmt = mysqli_prepare($conn, "UPDATE barang_kasir SET stok = stok - ? WHERE id_barangK = ? AND stok >= ?"); + + if (!$stmt) { + throw new Exception("Failed to prepare stock update statement: " . mysqli_error($conn)); + } + + mysqli_stmt_bind_param($stmt, "iii", $item['quantity'], $item['product_id'], $item['quantity']); + $executed = mysqli_stmt_execute($stmt); + + if (!$executed) { + throw new Exception("Failed to execute stock update statement: " . mysqli_stmt_error($stmt)); + } + + // Check if stock was actually updated (prevents overselling) + if (mysqli_affected_rows($conn) == 0) { + throw new Exception("Insufficient stock for product: " . $product['nama_barang']); + } + } + + // Commit transaction + mysqli_commit($conn); + + return [ + 'success' => true, + 'transaction_id' => $transactionId, + 'transaction_code' => $transactionCode, + 'cash_amount' => $cashAmount, + 'change_amount' => $changeAmount + ]; + } catch (Exception $e) { + // Rollback in case of error + mysqli_rollback($conn); + error_log("Transaction failed: " . $e->getMessage()); + return [ + 'success' => false, + 'message' => $e->getMessage() + ]; + } +} + +/** + * Get transaction by ID + * + * @param int $transactionId The transaction ID + * @return array|null Transaction details or null if not found + */ +function getTransactionById($transactionId) { + global $conn; + + try { + // Get transaction details + $stmt = mysqli_prepare($conn, "SELECT * FROM transaksi WHERE id_transaksi = ?"); + + if (!$stmt) { + error_log("Query preparation failed: " . mysqli_error($conn)); + return null; + } + + mysqli_stmt_bind_param($stmt, "i", $transactionId); + $executed = mysqli_stmt_execute($stmt); + + if (!$executed) { + error_log("Query execution failed: " . mysqli_stmt_error($stmt)); + mysqli_stmt_close($stmt); + return null; + } + + $result = mysqli_stmt_get_result($stmt); + $transaction = mysqli_fetch_assoc($result); + + if (!$transaction) { + return null; + } + + // Get transaction items + $stmt = mysqli_prepare($conn, " + SELECT dt.*, b.nama_barang, b.kode_barang, j.nama_jenis + FROM detail_transaksi dt + JOIN barang_kasir b ON dt.id_barangK = b.id_barangK + JOIN jenis_barang j ON b.id_jenis = j.id_jenis + WHERE dt.id_transaksi = ? + "); + + if (!$stmt) { + error_log("Query preparation failed: " . mysqli_error($conn)); + return $transaction; // Return transaction without items + } + + mysqli_stmt_bind_param($stmt, "i", $transactionId); + $executed = mysqli_stmt_execute($stmt); + + if (!$executed) { + error_log("Query execution failed: " . mysqli_stmt_error($stmt)); + mysqli_stmt_close($stmt); + return $transaction; // Return transaction without items + } + + $result = mysqli_stmt_get_result($stmt); + + $items = []; + while ($row = mysqli_fetch_assoc($result)) { + $items[] = $row; + } + + $transaction['items'] = $items; + + return $transaction; + } catch (Exception $e) { + error_log("Exception in getTransactionById: " . $e->getMessage()); + return null; + } +} + +/** + * Get recent transactions + * + * @param int $limit Maximum number of transactions to return + * @return array List of recent transactions + */ +function getRecentTransactions($limit = 10) { + global $conn; + + try { + $query = "SELECT * FROM transaksi ORDER BY created_at DESC LIMIT " . intval($limit); + $result = mysqli_query($conn, $query); + + if (!$result) { + error_log("Query failed: " . mysqli_error($conn)); + return []; + } + + $transactions = []; + while ($row = mysqli_fetch_assoc($result)) { + $transactions[] = $row; + } + + return $transactions; + } catch (Exception $e) { + error_log("Exception in getRecentTransactions: " . $e->getMessage()); + return []; + } +} + +/** + * Get current user information + * + * @return array|null User details or null if not found + */ +function getCurrentUser() { + global $conn; + + try { + if (isset($_SESSION['user_id'])) { + $stmt = mysqli_prepare($conn, "SELECT id_kasir, username, role, phone FROM kasir WHERE id_kasir = ?"); + + if (!$stmt) { + error_log("Query preparation failed: " . mysqli_error($conn)); + return null; + } + + mysqli_stmt_bind_param($stmt, "i", $_SESSION['user_id']); + $executed = mysqli_stmt_execute($stmt); + + if (!$executed) { + error_log("Query execution failed: " . mysqli_stmt_error($stmt)); + mysqli_stmt_close($stmt); + return null; + } + + $result = mysqli_stmt_get_result($stmt); + $user = mysqli_fetch_assoc($result); + mysqli_stmt_close($stmt); + + return $user; + } + return null; + } catch (Exception $e) { + error_log("Exception in getCurrentUser: " . $e->getMessage()); + return null; + } +} + +/** + * Get product by barcode (kode_barang) + * + * @param string $barcode The product barcode + * @return array|null Product details or null if not found + */ +function getProductByBarcode($barcode) { + global $conn; + + try { + $stmt = mysqli_prepare($conn, "SELECT b.*, j.nama_jenis FROM barang_kasir b + LEFT JOIN jenis_barang j ON b.id_jenis = j.id_jenis + WHERE b.kode_barang = ?"); + if (!$stmt) { + error_log("Query preparation failed: " . mysqli_error($conn)); + return null; + } + + mysqli_stmt_bind_param($stmt, "s", $barcode); + $executed = mysqli_stmt_execute($stmt); + + if (!$executed) { + error_log("Query execution failed: " . mysqli_stmt_error($stmt)); + mysqli_stmt_close($stmt); + return null; + } + + $result = mysqli_stmt_get_result($stmt); + $product = mysqli_fetch_assoc($result); + mysqli_stmt_close($stmt); + + // Log untuk debugging + if ($product) { + error_log("Product found: ID=" . $product['id_barangK'] . ", Name=" . $product['nama_barang']); + } else { + error_log("Product not found for barcode: " . $barcode); + } + + return $product; + } catch (Exception $e) { + error_log("Exception in getProductByBarcode: " . $e->getMessage()); + return null; + } +} \ No newline at end of file diff --git a/views/transaction/get_product_by_barcode.php b/views/transaction/get_product_by_barcode.php new file mode 100644 index 0000000..9015f18 --- /dev/null +++ b/views/transaction/get_product_by_barcode.php @@ -0,0 +1,245 @@ + false, + 'message' => 'Terjadi kesalahan internal. Silakan coba lagi.', + 'product' => null, + 'cart_html' => '', + 'cart_totals' => [ + 'items' => 0, + 'total' => 0, + 'formatted_total' => '0' + ] + ]); + return true; +} +set_error_handler('handleErrors'); + +// Try-catch untuk menangkap semua exception +try { + // Pastikan sesi dimulai + if (session_status() === PHP_SESSION_NONE) { + session_start(); + } + + // Log request untuk debugging + error_log("Barcode scan request received: " . json_encode($_POST)); + + // Initialize response + $response = [ + 'success' => false, + 'message' => 'Barcode tidak valid', + 'product' => null, + 'cart_html' => '', + 'cart_totals' => [ + 'items' => 0, + 'total' => 0, + 'formatted_total' => '0' + ] + ]; + + // Check if barcode is provided + if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['barcode'])) { + $barcode = trim($_POST['barcode']); + + // Validate barcode + if (empty($barcode)) { + $response['message'] = "Barcode tidak boleh kosong"; + sendResponse($response); + } + + // Get product by barcode (kode_barang) + $product = getProductByBarcode($barcode); + + if (!$product) { + $response['message'] = "Produk tidak ditemukan untuk barcode: " . htmlspecialchars($barcode); + sendResponse($response); + } + + // Check stock availability + if ($product['stok'] <= 0) { + $response['message'] = "Produk " . htmlspecialchars($product['nama_barang']) . " stok habis"; + sendResponse($response); + } + + // Initialize cart if not exists + if (!isset($_SESSION['cart']) || !is_array($_SESSION['cart'])) { + $_SESSION['cart'] = []; + error_log("Cart initialized"); + } + + $productId = $product['id_barangK']; + $quantity = 1; // Default quantity is 1 + + // Check if product already in cart, update quantity if exists + $exists = false; + foreach ($_SESSION['cart'] as $key => $item) { + if ($item['id'] == $productId) { + // Only add if it won't exceed stock + if ($item['quantity'] < $product['stok']) { + $_SESSION['cart'][$key]['quantity'] += 1; + } else { + // Stock limit reached + $response['success'] = false; + $response['message'] = "Tidak dapat menambahkan unit lagi. Batas stok tercapai."; + sendResponse($response); + } + $exists = true; + break; + } + } + + // If not exists, add to cart + if (!$exists) { + $_SESSION['cart'][] = [ + 'id' => $productId, + 'name' => $product['nama_barang'], + 'code' => $product['kode_barang'], + 'price' => $product['harga'], + 'quantity' => $quantity, + 'max_stock' => $product['stok'] + ]; + } + + // Set success response + $response['success'] = true; + $response['message'] = 'Produk ditambahkan ke keranjang'; + $response['product'] = [ + 'id' => $product['id_barangK'], + 'name' => $product['nama_barang'], + 'code' => $product['kode_barang'], + 'price' => $product['harga'], + 'stock' => $product['stok'] + ]; + + // Generate cart HTML + $response['cart_html'] = generateCartHTML(); + + // Calculate cart totals + $cartItems = count($_SESSION['cart']); + $total = 0; + foreach ($_SESSION['cart'] as $item) { + $total += $item['price'] * $item['quantity']; + } + + $response['cart_totals'] = [ + 'items' => $cartItems, + 'total' => $total, + 'formatted_total' => number_format($total, 0, ',', '.') + ]; + + // Log cart state for debugging + error_log("Cart updated. Items: " . $cartItems . ", Total: " . $total); + } + + // Send the response + sendResponse($response); + +} catch (Exception $e) { + // Log exception + error_log("Exception in get_product_by_barcode.php: " . $e->getMessage()); + + // Return error response + sendResponse([ + 'success' => false, + 'message' => 'Terjadi kesalahan: ' . $e->getMessage(), + 'product' => null, + 'cart_html' => '', + 'cart_totals' => [ + 'items' => 0, + 'total' => 0, + 'formatted_total' => '0' + ] + ]); +} + +/** + * Generate HTML for cart items + * + * @return string HTML content for cart + */ +function generateCartHTML() { + if (isset($_SESSION['cart']) && !empty($_SESSION['cart'])) { + $html = ''; + foreach ($_SESSION['cart'] as $index => $item) { + $html .= ' + '; + } + + // Add script to enable functionalities + $html .= ' + '; + + return $html; + } else { + return '

Keranjang Anda kosong.

'; + } +} \ No newline at end of file diff --git a/views/transaction/index.php b/views/transaction/index.php new file mode 100644 index 0000000..6d483e0 --- /dev/null +++ b/views/transaction/index.php @@ -0,0 +1,1147 @@ + $item) { + if ($item['id'] == $productId) { + // Ensure we don't exceed stock + $newQuantity = $item['quantity'] + $quantity; + if ($newQuantity <= $product['stok']) { + $_SESSION['cart'][$key]['quantity'] = $newQuantity; + } else { + $_SESSION['error_message'] = "Tidak dapat menambahkan {$quantity} unit {$product['nama_barang']}. Total melebihi stok tersedia ({$product['stok']})."; + header('Location: index.php'); + exit; + } + $exists = true; + break; + } + } + + // If not exists, add to cart + if (!$exists) { + $_SESSION['cart'][] = [ + 'id' => $productId, + 'name' => $product['nama_barang'], + 'code' => $product['kode_barang'], + 'price' => $product['harga'], + 'quantity' => $quantity, + 'max_stock' => $product['stok'] + ]; + } + } else { + // Set error message for stock limit + $_SESSION['error_message'] = "Tidak dapat menambahkan {$quantity} unit {$product['nama_barang']}. Hanya {$product['stok']} tersedia."; + } + } else { + $_SESSION['error_message'] = "Produk tidak ditemukan."; + } + + // Redirect to avoid form resubmission + header('Location: index.php'); + exit; +} + +// Process adding multiple products to cart +if (isset($_POST['product_ids']) && is_array($_POST['product_ids'])) { + // Log data for debugging + error_log('Adding multiple products. product_ids: ' . print_r($_POST['product_ids'], true)); + + foreach ($_POST['product_ids'] as $productId) { + // Validate product ID + $productId = (int)$productId; + if ($productId <= 0) continue; + + // Get product details + $product = getProductById($productId); + + if ($product) { + // Check stock availability + if ($product['stok'] > 0) { + // Check if product already in cart, update quantity if exists + $exists = false; + foreach ($_SESSION['cart'] as $key => $item) { + if ($item['id'] == $productId) { + // Only add if it won't exceed stock + if ($item['quantity'] < $product['stok']) { + $_SESSION['cart'][$key]['quantity'] += 1; // Default quantity is 1 + } + $exists = true; + break; + } + } + + // If not exists, add to cart + if (!$exists) { + $_SESSION['cart'][] = [ + 'id' => $productId, + 'name' => $product['nama_barang'], + 'code' => $product['kode_barang'], + 'price' => $product['harga'], + 'quantity' => 1, + 'max_stock' => $product['stok'] + ]; + } + } else { + // Log out-of-stock items + error_log("Produk {$product['nama_barang']} (ID: {$productId}) stok habis."); + } + } + } + + // Redirect to avoid form resubmission + header('Location: index.php'); + exit; +} + +// Update cart item quantity +if (isset($_POST['update_cart']) && isset($_POST['product_id'])) { + $productId = (int)$_POST['product_id']; + $quantity = isset($_POST['quantity']) ? (int)$_POST['quantity'] : 1; + + // Validate quantity + if ($quantity < 1) { + $quantity = 1; + } + + // Get product details for stock check + $product = getProductById($productId); + + if ($product) { + // Make sure quantity doesn't exceed stock + if ($quantity > $product['stok']) { + $quantity = $product['stok']; + $_SESSION['error_message'] = "Kuantitas disesuaikan ke stok maksimum yang tersedia ({$product['stok']})."; + } + + // Update quantity in cart + foreach ($_SESSION['cart'] as $key => $item) { + if ($item['id'] == $productId) { + $_SESSION['cart'][$key]['quantity'] = $quantity; + $_SESSION['cart'][$key]['max_stock'] = $product['stok']; // Update max stock in case it changed + break; + } + } + } + + header('Location: index.php'); + exit; +} + +// Remove item from cart +if (isset($_GET['remove_item'])) { + $index = $_GET['remove_item']; + if (isset($_SESSION['cart'][$index])) { + unset($_SESSION['cart'][$index]); + $_SESSION['cart'] = array_values($_SESSION['cart']); // Re-index array + } + + header('Location: index.php'); + exit; +} + +// Clear cart +if (isset($_GET['clear_cart'])) { + $_SESSION['cart'] = []; + header('Location: index.php'); + exit; +} + +// Process checkout +if (isset($_POST['checkout'])) { + // Log untuk debugging + error_log("Checkout process started: " . json_encode($_POST)); + + $items = []; + $total = 0; + + // Validasi keranjang + if (!isset($_SESSION['cart']) || empty($_SESSION['cart'])) { + $_SESSION['error_message'] = "Keranjang Anda kosong. Silakan tambahkan item ke keranjang."; + header('Location: index.php'); + exit; + } + + foreach ($_SESSION['cart'] as $item) { + $items[] = [ + 'product_id' => $item['id'], + 'quantity' => $item['quantity'] + ]; + $total += $item['price'] * $item['quantity']; + } + + // Get cash and change amount + $cashAmount = isset($_POST['cash_amount']) ? (float)$_POST['cash_amount'] : 0; + $changeAmount = isset($_POST['change_amount']) ? (float)$_POST['change_amount'] : 0; + + // Validate cash amount + if ($cashAmount < $total) { + $_SESSION['error_message'] = "Jumlah tunai harus sama dengan atau lebih besar dari jumlah total."; + header('Location: index.php'); + exit; + } + + // Log transaction details for debugging + error_log("Processing checkout: Items: " . count($items) . ", Total: $total, Cash: $cashAmount, Change: $changeAmount"); + + // Create transaction with cash and change amounts + $result = createTransaction($items, $total, $cashAmount, $changeAmount); + + if ($result['success']) { + // Log success for debugging + error_log("Transaction successful. ID: " . $result['transaction_id']); + + // Clear cart after successful checkout + $_SESSION['cart'] = []; + $_SESSION['last_transaction'] = $result; + + // Redirect to success page + header('Location: transaction_success.php?id=' . $result['transaction_id']); + exit; + } else { + // Log error for debugging + error_log("Transaction failed: " . $result['message']); + + $error = $result['message']; + $_SESSION['error_message'] = $error; + header('Location: index.php'); + exit; + } +} + +// Get all product types +$productTypes = getProductTypes(); + +// Handle search query +$searchQuery = isset($_GET['search']) ? trim($_GET['search']) : ''; + +// Get products by selected product type or all products +$selectedType = isset($_GET['type']) ? $_GET['type'] : null; +$products = getProducts($selectedType, $searchQuery); + +// Calculate cart totals +$cartItems = count($_SESSION['cart']); +$total = 0; +foreach ($_SESSION['cart'] as $item) { + $total += $item['price'] * $item['quantity']; +} +?> + + + + + + + + + + + + Ayula Store - POS + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+
0 produk terseleksi
+
+ + +
+
+ + + + + + + +
+
+ + + + + + + + + + + + + +
+ + + +
+
+
+
+ + +
+
+
Pindai Barcode
+ +
+
+
+ + + + +
+
+ Pindai disini +
+
+
+ + + +
+
+ Hasil pencarian untuk: + ( produk ditemukan) +
+
+ + + +
+
+
+ 0): ?> + +
+
+
+ + <?php echo $product['nama_barang']; ?> + + default + +
Stok:
+
+ + +
+
+
+
+

+
Rp.
+
+ + + +
+
+
+
+ + +
+

Tidak ada barang ditemukan.

+
+ +
+
+
+
+ +
+ +
+
+

Keranjang Belanja

+
Transaksi sedang berlangsung
+
+
+ + +
+ + + + +
+ + $item): ?> +
    +
  • +
    +
    + img +
    +
    +

    +
    +
    +
    +
    +
    +
    + + +
    + + + + +
    +
    +
    +
    +
    +
    +
  • +
  • Rp.
  • +
  • + + img + +
  • +
+ + +

Keranjang Anda kosong.

+ +
+ + +
+
+
    +
  • +
    Total
    +
    Rp.
    +
  • +
+
+ +
+
    +
  • +
    + img + Tunai +
    +
  • +
+
+ + +
+
+ +
+ Rp. + +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + + + + + +
+ + + + +
+ + + +
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/views/transaction/transaction.js b/views/transaction/transaction.js new file mode 100644 index 0000000..32b4735 --- /dev/null +++ b/views/transaction/transaction.js @@ -0,0 +1,482 @@ +/** + * Ayula Store POS System + * Main JavaScript file for transaction page functionality + */ + +$(document).ready(function() { + // Initialize globals + initializeGlobals(); + + // Setup event handlers + setupEventHandlers(); + + // Initialize UI components + initializeUI(); + + // Check and update the "Clear All" button state on page load + updateClearCartButtonInitialState(); +}); + +/** + * Initialize global variables and state + */ +function initializeGlobals() { + // Variables to track selected products + window.selectedProducts = []; + window.multiSelectToolbar = $('.multi-select-toolbar'); + + // Store product stock information + window.productStock = {}; + $('.productset').each(function() { + const productId = $(this).data('product-id'); + const stockText = $(this).find('.productsetimg h6').text(); + const stockValue = parseInt(stockText.replace('Stok: ', '')) || 0; + window.productStock[productId] = stockValue; + }); +} + +/** + * Check and update the "Clear All" button state on page load + */ +function updateClearCartButtonInitialState() { + const cartItemsCount = $('.product-lists').length; + console.log('Initial cart items count:', cartItemsCount); + + // Get the clear cart button + const clearCartBtn = $('#clear-cart-btn'); + + if (cartItemsCount > 0) { + // Enable the clear cart button + clearCartBtn.attr('href', 'javascript:void(0);') + .removeClass('disabled') + .css({ + 'opacity': '1', + 'cursor': 'pointer' + }); + + console.log('Clear cart button initially enabled'); + } else { + // Disable the clear cart button + clearCartBtn.attr('href', '#') + .addClass('disabled') + .css({ + 'opacity': '0.5', + 'cursor': 'not-allowed' + }); + + console.log('Clear cart button initially disabled'); + } +} + +/** + * Setup all event handlers for the page + */ +function setupEventHandlers() { + // Product card selection + setupProductSelection(); + + // Toolbar actions + setupToolbarActions(); +} + +/** + * Initialize UI components + */ +function initializeUI() { + // Hide all check marks initially + $('.check-product i').hide(); + + // Trigger toggle button click to activate it when page loads + setTimeout(function() { + $("#toggle_btn").trigger('click'); + }, 100); + + // Focus search field when search icon is clicked + $('.responsive-search').on('click', function() { + setTimeout(function() { + $('input[name="search"]').focus(); + }, 100); + }); + + // Focus the page header search field on click + $('.product-search-form input[name="search"]').on('click', function() { + $(this).focus(); + }); + + // Enable live search functionality + setupLiveSearch(); + + // Enable AJAX category navigation + setupCategoryNavigation(); + + // Initialize cash payment functionality + setupCashPayment(); +} + +/** + * Setup product selection functionality + */ +function setupProductSelection() { + // Click on product card to toggle selection + $('.productset').on('click', function(e) { + // Only handle clicks on the card itself, not buttons or links inside + if ($(e.target).closest('button, a, form').length === 0) { + const productId = $(this).data('product-id'); + const checkbox = $(this).find('.product-checkbox'); + const isChecked = checkbox.prop('checked'); + + console.log('Clicked product ID:', productId); + + // Toggle checkbox + checkbox.prop('checked', !isChecked); + + if (!isChecked) { + // Add to selected list and highlight + window.selectedProducts.push(productId); + $(this).addClass('selected'); + $(this).find('.check-product i').show(); + } else { + // Remove from selected list and unhighlight + window.selectedProducts = window.selectedProducts.filter(id => id !== productId); + $(this).removeClass('selected'); + $(this).find('.check-product i').hide(); + } + + console.log('Selected products:', window.selectedProducts); + updateToolbar(); + } + }); +} + +/** + * Setup toolbar action buttons + */ +function setupToolbarActions() { + // Cancel selection button + $('#cancel-selection').on('click', function() { + // Uncheck all checkboxes and hide check marks + $('.product-checkbox').prop('checked', false); + $('.productset').removeClass('selected'); + $('.check-product i').hide(); + window.selectedProducts = []; + updateToolbar(); + }); + + // Add selected products to cart + $('#add-selected-to-cart').on('click', function() { + if (window.selectedProducts.length > 0) { + console.log('Adding products to cart:', window.selectedProducts); + + // Create a form to submit selected products + const form = $('
', { + method: 'post', + action: 'index.php' + }); + + // Add each product ID as a hidden input + window.selectedProducts.forEach(function(productId) { + form.append( + $('').attr({ + type: 'hidden', + name: 'product_ids[]', + value: productId + }) + ); + }); + + // Log form contents before submission + console.log('Form contents:', form.serialize()); + + // Add the form to the body and submit it + $('body').append(form); + form.submit(); + } + }); +} + +/** + * Update the toolbar state based on selected products + */ +function updateToolbar() { + if (window.selectedProducts.length > 0) { + $('.selected-count').text(window.selectedProducts.length + ' item' + (window.selectedProducts.length > 1 ? 's' : '') + ' selected'); + window.multiSelectToolbar.addClass('active'); + } else { + window.multiSelectToolbar.removeClass('active'); + } +} + +/** + * Setup live search functionality + */ +function setupLiveSearch() { + // Handle both search inputs - top nav and product header search + const searchInputs = $('input[name="search"]'); + + // Store the original URL for reference + const originalUrl = window.location.href.split('?')[0]; + const urlParams = new URLSearchParams(window.location.search); + + // Clear search buttons functionality + $('.product-search-form a, .search-addon a').on('click', function(e) { + e.preventDefault(); + urlParams.delete('search'); + + // Build the new URL + let newUrl = originalUrl; + const paramString = urlParams.toString(); + if (paramString) { + newUrl += '?' + paramString; + } + + // Navigate to the new URL + window.location.href = newUrl; + }); + + // Custom form submission to prevent loader + $('.product-search-form form, .top-nav-search form').on('submit', function(e) { + e.preventDefault(); // Prevent the default form submission + + // Get the search query + const searchQuery = $(this).find('input[name="search"]').val().trim(); + + // Get any type filter + const typeParam = $(this).find('input[name="type"]').val(); + + // Build the URL + let newUrl = originalUrl; + if (searchQuery || typeParam) { + newUrl += '?'; + + if (searchQuery) { + newUrl += 'search=' + encodeURIComponent(searchQuery); + if (typeParam) { + newUrl += '&'; + } + } + + if (typeParam) { + newUrl += 'type=' + encodeURIComponent(typeParam); + } + } + + // Use AJAX to fetch the page content + $.ajax({ + url: newUrl, + type: 'GET', + beforeSend: function() { + // Add a simple loading indicator to the product area + $('.tab_content').addClass('loading'); + $('.tab_content > .row').css('opacity', '0.5'); + }, + success: function(response) { + // Extract and replace just the product content + const $response = $(response); + const newProductContent = $response.find('.tab_content').html(); + + // Update the DOM with new content + $('.tab_content').html(newProductContent); + + // Update browser URL without reloading + history.pushState({}, '', newUrl); + + // Update search info area if it exists + const searchInfoContent = $response.find('.search-results-info').html(); + if (searchInfoContent) { + if ($('.search-results-info').length) { + $('.search-results-info').html(searchInfoContent); + } else { + $('
') + .html(searchInfoContent) + .insertAfter('.page-header'); + } + $('.search-results-info').show(); + } else { + $('.search-results-info').hide(); + } + + // Update the other search input with the same value + searchInputs.val(searchQuery); + + // Reset the loading state + $('.tab_content').removeClass('loading'); + $('.tab_content > .row').css('opacity', '1'); + + // Reinitialize product selection for newly loaded products + setupProductSelection(); + }, + error: function() { + // If something goes wrong, just do a normal page load + window.location.href = newUrl; + } + }); + }); +} + +/** + * Setup AJAX-based category navigation + */ +function setupCategoryNavigation() { + // Original URL for reference + const originalUrl = window.location.href.split('?')[0]; + + // Add click event handlers to all category links + $('.tabs li a.category-tab').on('click', function(e) { + e.preventDefault(); + + // Get category type from URL + const href = $(this).attr('href'); + const url = new URL(href, window.location.origin); + const typeParam = url.searchParams.get('type'); + + // Get current search query if any + const currentUrl = new URL(window.location.href); + const searchQuery = currentUrl.searchParams.get('search'); + + // Build the target URL + let targetUrl = originalUrl; + const params = new URLSearchParams(); + + // Add type parameter if set + if (typeParam) { + params.set('type', typeParam); + } + + // Preserve search query if exists + if (searchQuery) { + params.set('search', searchQuery); + } + + // Append parameters to URL if any + const paramString = params.toString(); + if (paramString) { + targetUrl += '?' + paramString; + } + + // Highlight the active category tab + $('.tabs li').removeClass('active'); + $(this).closest('li').addClass('active'); + + // Use AJAX to fetch the category products + $.ajax({ + url: targetUrl, + type: 'GET', + beforeSend: function() { + // Add loading indicator to product area + $('.tab_content').addClass('loading'); + $('.tab_content > .row').css('opacity', '0.5'); + }, + success: function(response) { + // Extract and replace just the product content + const $response = $(response); + const newProductContent = $response.find('.tab_content').html(); + + // Update the DOM with new content + $('.tab_content').html(newProductContent); + + // Update browser URL without reloading + history.pushState({}, '', targetUrl); + + // Reset the loading state + $('.tab_content').removeClass('loading'); + $('.tab_content > .row').css('opacity', '1'); + + // Reinitialize product selection for newly loaded products + setupProductSelection(); + }, + error: function() { + // If something goes wrong, just do a normal page load + window.location.href = targetUrl; + } + }); + }); +} + +/** + * Setup cash payment functionality + */ +function setupCashPayment() { + const cashInput = $('#cash-amount'); + const changeDisplay = $('#change-amount'); + const changeContainer = $('#change-container'); + const hiddenCashAmount = $('#hidden-cash-amount'); + const hiddenChangeAmount = $('#hidden-change-amount'); + const totalAmount = parseFloat($('.total-value h6').text().replace(/[^\d]/g, '')); + + // Format cash input with thousand separators + cashInput.on('input', function() { + // Remove non-numeric characters + let value = $(this).val().replace(/[^\d]/g, ''); + + // Convert to number and format with thousand separator + if (value !== '') { + const numericValue = parseInt(value); + + // Calculate change + const change = numericValue - totalAmount; + + // Update hidden fields for form submission + hiddenCashAmount.val(numericValue); + hiddenChangeAmount.val(Math.max(0, change)); + + // Format display value + $(this).val(formatNumber(numericValue)); + + // Show change if payment is sufficient + if (change >= 0) { + changeDisplay.text('Rp. ' + formatNumber(change)); + changeContainer.show(); + } else { + changeContainer.hide(); + } + } else { + $(this).val(''); + changeContainer.hide(); + } + }); + + // Quick cash buttons + $('.quick-cash').on('click', function() { + const value = $(this).data('value'); + cashInput.val(formatNumber(value)); + + // Calculate change + const change = value - totalAmount; + + // Update hidden fields for form submission + hiddenCashAmount.val(value); + hiddenChangeAmount.val(Math.max(0, change)); + + // Show change + if (change >= 0) { + changeDisplay.text('Rp. ' + formatNumber(change)); + changeContainer.show(); + } else { + changeContainer.hide(); + } + }); + + // Form submission check + $('#checkout-form').on('submit', function(e) { + const cashValue = parseFloat(hiddenCashAmount.val()); + + // Check if cash amount is sufficient + if (cashValue < totalAmount) { + e.preventDefault(); + alert('Cash amount is not sufficient!'); + return false; + } + + return true; + }); + + // Trigger input event to initialize on page load + cashInput.trigger('input'); +} + +/** + * Format number with thousand separator + */ +function formatNumber(number) { + return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, "."); +} \ No newline at end of file diff --git a/views/transaction/transaction_success.php b/views/transaction/transaction_success.php new file mode 100644 index 0000000..711a587 --- /dev/null +++ b/views/transaction/transaction_success.php @@ -0,0 +1,453 @@ + + + + + + + + + + + + Ayula Store POS - Transaksi Sukses + + + + + + + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + +
+ + + +
+
+
+
+
+
+
+

Transaksi Berhasil

+

Transaksi Anda telah diproses dengan sukses

+
+
+ +
+
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + +
BarangJumlahHargaTotal
+
+
+
+

+
+
+
Rp Rp
+
+ +
+
+
+
+
+
Total
+
Rp
+
+ + + 0): ?> +
+
Jumlah Tunai
+
Rp
+
+
+
Kembalian
+
Rp
+
+ +
+
+
+
+ +
+ Transaksi Baru + +
+
+
+
+
+
+
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/views/uploads/img-barang/19351527_4100_2_02.jpg b/views/uploads/img-barang/19351527_4100_2_02.jpg new file mode 100644 index 0000000..5d30d7b Binary files /dev/null and b/views/uploads/img-barang/19351527_4100_2_02.jpg differ diff --git a/views/uploads/img-barang/Snipaste_2025-06-03_06-11-33.png b/views/uploads/img-barang/Snipaste_2025-06-03_06-11-33.png new file mode 100644 index 0000000..2fc3e57 Binary files /dev/null and b/views/uploads/img-barang/Snipaste_2025-06-03_06-11-33.png differ diff --git a/views/uploads/img-barang/image (6).png b/views/uploads/img-barang/image (6).png new file mode 100644 index 0000000..82883f3 Binary files /dev/null and b/views/uploads/img-barang/image (6).png differ diff --git a/views/uploads/img-barang/jiwoo.jpg b/views/uploads/img-barang/jiwoo.jpg new file mode 100644 index 0000000..a93218f Binary files /dev/null and b/views/uploads/img-barang/jiwoo.jpg differ diff --git a/views/uploads/img-barang/kios-Halaman-2.drawio (2).png b/views/uploads/img-barang/kios-Halaman-2.drawio (2).png new file mode 100644 index 0000000..87b0c40 Binary files /dev/null and b/views/uploads/img-barang/kios-Halaman-2.drawio (2).png differ diff --git a/views/uploads/img-barang/kios-Halaman-3.drawio (2).png b/views/uploads/img-barang/kios-Halaman-3.drawio (2).png new file mode 100644 index 0000000..90a95d4 Binary files /dev/null and b/views/uploads/img-barang/kios-Halaman-3.drawio (2).png differ diff --git a/views/uploads/img-barang/kios-Halaman-3.drawio (3).png b/views/uploads/img-barang/kios-Halaman-3.drawio (3).png new file mode 100644 index 0000000..9fd7b6b Binary files /dev/null and b/views/uploads/img-barang/kios-Halaman-3.drawio (3).png differ diff --git a/views/uploads/img-barang/kios-Page-1.drawio (2).png b/views/uploads/img-barang/kios-Page-1.drawio (2).png new file mode 100644 index 0000000..1b8dde8 Binary files /dev/null and b/views/uploads/img-barang/kios-Page-1.drawio (2).png differ diff --git a/views/uploads/img-barang/online-shopping.png b/views/uploads/img-barang/online-shopping.png new file mode 100644 index 0000000..ced34dd Binary files /dev/null and b/views/uploads/img-barang/online-shopping.png differ diff --git a/views/uploads/img-barang/product_1748803380.png b/views/uploads/img-barang/product_1748803380.png new file mode 100644 index 0000000..90a95d4 Binary files /dev/null and b/views/uploads/img-barang/product_1748803380.png differ diff --git a/views/uploads/img-barang/sawah kios.drawio (3).png b/views/uploads/img-barang/sawah kios.drawio (3).png new file mode 100644 index 0000000..a8e0466 Binary files /dev/null and b/views/uploads/img-barang/sawah kios.drawio (3).png differ diff --git a/views/uploads/nota/nota_1747116626_9284.PNG b/views/uploads/nota/nota_1747116626_9284.PNG new file mode 100644 index 0000000..451c96f Binary files /dev/null and b/views/uploads/nota/nota_1747116626_9284.PNG differ diff --git a/views/uploads/nota/nota_1748760835_6779.png b/views/uploads/nota/nota_1748760835_6779.png new file mode 100644 index 0000000..8c60db2 Binary files /dev/null and b/views/uploads/nota/nota_1748760835_6779.png differ diff --git a/views/uploads/nota/nota_1748804342_8076.png b/views/uploads/nota/nota_1748804342_8076.png new file mode 100644 index 0000000..a9b72a2 Binary files /dev/null and b/views/uploads/nota/nota_1748804342_8076.png differ diff --git a/views/uploads/nota/nota_1748804468_7902.png b/views/uploads/nota/nota_1748804468_7902.png new file mode 100644 index 0000000..c94cb33 Binary files /dev/null and b/views/uploads/nota/nota_1748804468_7902.png differ diff --git a/views/uploads/nota/nota_1748804899_5014.png b/views/uploads/nota/nota_1748804899_5014.png new file mode 100644 index 0000000..82883f3 Binary files /dev/null and b/views/uploads/nota/nota_1748804899_5014.png differ diff --git a/views/uploads/nota/nota_1748884253_9677.jpg b/views/uploads/nota/nota_1748884253_9677.jpg new file mode 100644 index 0000000..0b4507f Binary files /dev/null and b/views/uploads/nota/nota_1748884253_9677.jpg differ diff --git a/views/uploads/nota/nota_1748913674_6638.jpg b/views/uploads/nota/nota_1748913674_6638.jpg new file mode 100644 index 0000000..0b4507f Binary files /dev/null and b/views/uploads/nota/nota_1748913674_6638.jpg differ diff --git a/views/uploads/nota/nota_1752217991_4871.png b/views/uploads/nota/nota_1752217991_4871.png new file mode 100644 index 0000000..ed10028 Binary files /dev/null and b/views/uploads/nota/nota_1752217991_4871.png differ diff --git a/views/users/add-user.php b/views/users/add-user.php new file mode 100644 index 0000000..72acd19 --- /dev/null +++ b/views/users/add-user.php @@ -0,0 +1,293 @@ + 13) { + $showUsernameModal = true; + $modalMessage = "Nomor telepon harus terdiri dari 11 hingga 13 angka."; + } + // Check if passwords match + else if ($password !== $confirmPassword) { + $showUsernameModal = true; + $modalMessage = "Password tidak sama."; + } else { + // Check if username already exists + $checkUsername = "SELECT username FROM kasir WHERE username = ?"; + $checkStmt = $conn->prepare($checkUsername); + $checkStmt->bind_param("s", $newUsername); + $checkStmt->execute(); + $result = $checkStmt->get_result(); + + if ($result->num_rows > 0) { + // Set flag to show modal + $showUsernameModal = true; + $modalMessage = "Username sudah digunakan. Silakan pilih username lain."; + } else { + // Hash the password + $hashed_password = password_hash($password, PASSWORD_DEFAULT); + + // Insert the user information into the database + $insertSql = "INSERT INTO kasir (username, password, phone, role) VALUES (?, ?, ?, ?)"; + $insertStmt = $conn->prepare($insertSql); + $insertStmt->bind_param("ssss", $newUsername, $hashed_password, $phone, $role); + $insertStmt->execute(); + + // Redirect to the users list after the insert + header("Location: index.php"); + exit; + } + } + } else { + // Show error if any form field is empty + $showUsernameModal = true; + $modalMessage = "Harap isi semua kolom."; + } +} + +// Variable for modal display +$showUsernameModal = isset($showUsernameModal) ? $showUsernameModal : false; +$modalMessage = isset($modalMessage) ? $modalMessage : ""; + +$conn->close(); +?> + + + + + + + + + + + + Ayula Store - Tambah Pengguna + + + + + + + + + + + + +
+
+
+ +
+
+ + + + + + + + + + + + + + +
+
+ + +
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + + +
+
+ + +
+
+
+ + Batal +
+
+ +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/views/users/delete-user.php b/views/users/delete-user.php new file mode 100644 index 0000000..553b1aa --- /dev/null +++ b/views/users/delete-user.php @@ -0,0 +1,26 @@ +prepare($sql); + $stmt->bind_param("i", $id_kasir); + + // Execute the query + if ($stmt->execute()) { + // Redirect back to the user list after successful deletion + header("Location: index.php"); + exit; + } else { + // If deletion fails, show an error message + echo "Error deleting record: " . $conn->error; + } +} + +// Close the connection +$conn->close(); +?> diff --git a/views/users/edit-user.php b/views/users/edit-user.php new file mode 100644 index 0000000..36fd8fc --- /dev/null +++ b/views/users/edit-user.php @@ -0,0 +1,264 @@ +prepare($sql); + $stmt->bind_param("i", $id_kasir); // Bind the id to the query + $stmt->execute(); + $result = $stmt->get_result(); + + // Check if the user exists + if ($result->num_rows > 0) { + $user = $result->fetch_assoc(); + } else { + echo "Pengguna tidak ditemukan."; + exit; + } +} + +// Update user data if the form is submitted +if ($_SERVER['REQUEST_METHOD'] == 'POST') { + // Get the form data + $editUsername = $_POST['username']; + $password = $_POST['password']; + $phone = $_POST['phone']; + $role = $_POST['role']; + + // Validate form fields (optional but recommended) + if (!empty($editUsername) && !empty($phone) && !empty($role)) { + // Check if password is provided + if (!empty($password)) { + // Hash the password + $hashed_password = password_hash($password, PASSWORD_DEFAULT); + + // Update with new password + $updateSql = "UPDATE kasir SET username = ?, password = ?, phone = ?, role = ? WHERE id_kasir = ?"; + $updateStmt = $conn->prepare($updateSql); + $updateStmt->bind_param("ssssi", $editUsername, $hashed_password, $phone, $role, $id_kasir); + } else { + // Update without changing password + $updateSql = "UPDATE kasir SET username = ?, phone = ?, role = ? WHERE id_kasir = ?"; + $updateStmt = $conn->prepare($updateSql); + $updateStmt->bind_param("sssi", $editUsername, $phone, $role, $id_kasir); + } + + $updateStmt->execute(); + + // Redirect to the users list after the update + header("Location: index.php"); + exit; + } else { + // Show error if any form field is empty + echo ""; + } +} + +$conn->close(); +?> + + + + + + + + + + + + Ayula Store - Ubah Pengguna + + + + + + + + + + + + +
+
+
+ +
+
+ + + + + + + + + + + + + +
+ + + +
+
+ + +
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + Batal +
+
+
+
+
+ +
+
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/views/users/index.php b/views/users/index.php new file mode 100644 index 0000000..84d4e15 --- /dev/null +++ b/views/users/index.php @@ -0,0 +1,296 @@ +query($sql); + +if ($result->num_rows > 0) { + $kasirData = []; + while ($row = $result->fetch_assoc()) { + $kasirData[] = $row; + } +} else { + $kasirData = null; // Jika tidak ada data +} + +// Menutup koneksi +$conn->close(); +?> + + + + + + + Ayula Store - Pengguna + + + + + + + + + + + + +
+
+
+ +
+
+ + + + + + + + + + + + + +
+ + + +
+
+ + +
+
+
+ + + + + + + + + + + + + "; + echo ""; + echo ""; + echo ""; + + echo ""; + } + echo ""; + } + + ?> + +
NamaTeleponPeranAksi
" . $kasir['username'] . "" . $kasir['phone'] . "" . ($kasir['role'] == 'admin' ? 'Admin' : 'Karyawan') . " + + img + + + img + +
+
+
+
+
+
+ + + +
+ + + + + + + + + + + + + + + + + + \ No newline at end of file