diff --git a/.dockerignore b/.dockerignore
index bb72ff1..a5ea979 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -5,3 +5,6 @@ Dockerfile
docker-compose.yml
.git
.gitignore
+tests
+*.log
+storage/logs/*
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index d65ea2d..df8b434 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-# Stage 1: Build Vue
+# Stage 1: Build Vue (tetap sama)
FROM node:20 as node_builder
WORKDIR /app
COPY package*.json ./
@@ -11,19 +11,24 @@ FROM php:8.3-fpm
RUN apt-get update && apt-get install -y \
git unzip libzip-dev libpng-dev libonig-dev libxml2-dev curl \
- && docker-php-ext-install pdo_mysql zip gd mbstring exif pcntl bcmath
+ && docker-php-ext-install pdo_mysql zip gd mbstring exif pcntl bcmath \
+ && apt-get clean && rm -rf /var/lib/apt/lists/* # Cleanup untuk ukuran kecil
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
-# Copy source code KECUALI public (biar ga ketiban build Vue)
+# Copy source code
COPY . .
-# Copy hasil build Vue dari stage 1
+# Copy hasil build Vue
COPY --from=node_builder /app/dist /var/www/html/public
RUN composer install --no-dev --optimize-autoloader
-RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache
+RUN php artisan config:cache && php artisan route:cache && php artisan view:cache # Optimasi cache untuk performa laporan/transaksi
+
+# Set permission dan user non-root
+RUN chown -R www-data:www-data /var/www/html
+USER www-data
EXPOSE 9000
CMD ["php-fpm"]
diff --git a/README.md b/README.md
index bdda337..ed90ee8 100644
--- a/README.md
+++ b/README.md
@@ -276,6 +276,13 @@ php artisan backup:run
php artisan make:model ProductCategory -m
```
+### Production
+
+> Pastikan `.env.production` sudah ada.
+
+```bash
+docker compose --env-file .env.production up --build -d
+```
---
## 📄 License
diff --git a/docker-compose.yml b/docker-compose.yml
index 0acbbfb..df11581 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -3,16 +3,16 @@ services:
build:
context: .
dockerfile: Dockerfile
- container_name: laravel_app
+ container_name: laravel_app_prod
volumes:
- - .:/var/www/html
- command: php artisan serve --host=0.0.0.0 --port=8000
+ - ./storage:/var/www/html/storage
ports:
- - "8000:8000"
+ - "9000"
depends_on:
- mysql
environment:
- APP_ENV: local
+ APP_ENV: production
+ APP_DEBUG: false
APP_KEY: ${APP_KEY}
DB_CONNECTION: mysql
DB_HOST: mysql
@@ -21,19 +21,36 @@ services:
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
+ nginx:
+ image: nginx:alpine
+ container_name: nginx_prod
+ ports:
+ - "80:80"
+ volumes:
+ - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
+ - ./storage:/var/www/html/storage:ro
+ depends_on:
+ - laravel
+
mysql:
image: mysql:8
- container_name: mysql_db
+ container_name: mysql_db_prod
restart: unless-stopped
environment:
- MYSQL_ROOT_PASSWORD: root
- MYSQL_DATABASE: laravel
- MYSQL_USER: laravel
- MYSQL_PASSWORD: laravel
+ MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
+ MYSQL_DATABASE: ${DB_DATABASE}
+ MYSQL_USER: ${DB_USERNAME}
+ MYSQL_PASSWORD: ${DB_PASSWORD}
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
+ redis:
+ image: redis:alpine
+ container_name: redis_prod
+ ports:
+ - "6379:6379"
+
volumes:
mysql_data:
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 0000000..443a33e
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,21 @@
+server {
+ listen 80;
+ index index.php index.html;
+ error_log /var/log/nginx/error.log;
+ access_log /var/log/nginx/access.log;
+ root /var/www/html/public;
+
+ location / {
+ try_files $uri $uri/ /index.php?$query_string;
+ }
+
+ location ~ \.php$ {
+ try_files $uri =404;
+ fastcgi_split_path_info ^(.+\.php)(/.+)$;
+ fastcgi_pass laravel:9000;
+ fastcgi_index index.php;
+ include fastcgi_params;
+ fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+ fastcgi_param PATH_INFO $fastcgi_path_info;
+ }
+}
\ No newline at end of file
diff --git a/resources/js/components/StrukOverlay.vue b/resources/js/components/StrukOverlay.vue
index abdc190..06bd95f 100644
--- a/resources/js/components/StrukOverlay.vue
+++ b/resources/js/components/StrukOverlay.vue
@@ -196,8 +196,8 @@ import logo_bni from '@/../images/logo_bni.png'
import logo_mastercard from '@/../images/logo_mastercard.png'
import logo_visa from '@/../images/logo_visa.png'
import logo_mandiri from '@/../images/logo_mandiri.png'
-import inputField from '@/components/inputField.vue'
-import inputSelect from '@/components/inputSelect.vue'
+import inputField from '@/components/InputField.vue'
+import inputSelect from '@/components/InputSelect.vue'
import axios from 'axios'
diff --git a/resources/js/pages/Akun.vue b/resources/js/pages/Akun.vue
index d8c6f68..4eef43c 100644
--- a/resources/js/pages/Akun.vue
+++ b/resources/js/pages/Akun.vue
@@ -48,7 +48,26 @@
Tambah User
-
+
+
+
+ Error!
+ {{ alert.error }}
+
+
+ Success!
+ {{ alert.success }}
+
+
+
{
+ alert.value = null;
+ }, 5000);
+ }
// Fetch data dari API
const fetchAkun = async () => {
loading.value = true;
@@ -191,6 +219,7 @@ const confirmDelete = async () => {
});
fetchAkun();
confirmDeleteOpen.value = false;
+ showAlert("success", "User berhasil dihapus.");
} catch (error) {
console.error("Error deleting akun:", error);
}
@@ -205,11 +234,13 @@ const closeDeleteModal = () => {
const closeAkun = () => {
creatingAkun.value = false;
fetchAkun();
+ showAlert("success", "User berhasil ditambahkan.");
};
const closeEditAkun = () => {
editingAkun.value = false;
fetchAkun();
+ showAlert("success", "User berhasil diubah.");
};
// Lifecycle
diff --git a/resources/js/pages/EditProduk.vue b/resources/js/pages/EditProduk.vue
index 97ac7f8..9e57d13 100644
--- a/resources/js/pages/EditProduk.vue
+++ b/resources/js/pages/EditProduk.vue
@@ -288,7 +288,7 @@ const loadProduk = async () => {
});
const produk = response.data;
// console.log(produk);
-
+
form.value = {
nama: produk.nama,
id_kategori: produk.id_kategori,
@@ -310,7 +310,7 @@ const loadFoto = async () => {
// console.log(uploadedImages.value);
} catch (e) {
console.error(e);
-
+
uploadError.value = "Gagal memuat foto";
}
};
@@ -415,7 +415,7 @@ const submitForm = async () => {
},
}
);
- router.push("/produk");
+ router.push("/produk?message=Produk berhasil diperbarui");
} catch (err) {
errorMessages.value = err.response?.data?.message || "Gagal menyimpan produk";
console.error(err);
diff --git a/resources/js/pages/InputProduk.vue b/resources/js/pages/InputProduk.vue
index e264117..f497f7a 100644
--- a/resources/js/pages/InputProduk.vue
+++ b/resources/js/pages/InputProduk.vue
@@ -174,7 +174,7 @@ import CreateItemModal from "../components/CreateItemModal.vue";
const router = useRouter();
const form = ref({
- nama: '', id_kategori: null, berat: 0, kadar: 0, harga_per_gram: 0, harga_jual: 0,
+ nama: '', id_kategori: null, berat: null, kadar: null, harga_per_gram: null, harga_jual: null,
});
const category = ref([]);
const showUploadMenu = ref(false);
diff --git a/resources/js/pages/Kategori.vue b/resources/js/pages/Kategori.vue
index f1a1b66..0524b57 100644
--- a/resources/js/pages/Kategori.vue
+++ b/resources/js/pages/Kategori.vue
@@ -16,7 +16,26 @@
Tambah Kategori
-
+
+
+
+ Error!
+ {{ alert.error }}
+
+
+ Success!
+ {{ alert.success }}
+
+
+
@@ -96,9 +115,18 @@ const creatingKategori = ref(false);
const detail = ref(null);
const confirmDeleteOpen = ref(false);
const kategoriToDelete = ref(null);
-
+const alert = ref(null);
+const timer = ref(null);
const isAdmin = localStorage.getItem("role") === "owner";
+function showAlert(type, message) {
+ alert.value = { [type]: message };
+ clearTimeout(timer.value);
+ timer.value = setTimeout(() => {
+ alert.value = null;
+ }, 5000);
+ }
+
// Fetch data kategori dari API
const fetchKategoris = async () => {
loading.value = true;
@@ -109,7 +137,6 @@ const fetchKategoris = async () => {
},
});
kategori.value = response.data;
- // console.log("Data kategori:", response.data);
} catch (error) {
console.error("Error fetching kategori:", error);
} finally {
@@ -119,14 +146,19 @@ const fetchKategoris = async () => {
// Tambah kategori - open modal
const tambahKategori = () => {
- detail.value = null; // Reset detail untuk mode create
+ detail.value = null;
creatingKategori.value = true;
};
// Close modal
const closeKategori = () => {
creatingKategori.value = false;
- fetchKategoris(); // Refresh data setelah modal ditutup
+ fetchKategoris();
+ if (detail.value==null) {
+
+ showAlert("success", "Kategori berhasil ditambahkan");
+ } else
+ showAlert("success", "Kategori berhasil diubah");
};
// Ubah kategori
@@ -139,6 +171,7 @@ const ubahKategori = (item) => {
const hapusKategori = (item) => {
kategoriToDelete.value = item;
confirmDeleteOpen.value = true;
+ showAlert("success", "Kategori berhasil dihapus");
};
// 🔵 Ditambahkan: aksi konfirmasi hapus
diff --git a/resources/js/pages/Produk.vue b/resources/js/pages/Produk.vue
index 2911502..1675e45 100644
--- a/resources/js/pages/Produk.vue
+++ b/resources/js/pages/Produk.vue
@@ -1,11 +1,11 @@
-
@@ -51,6 +51,27 @@
+
+
+
+ Error!
+ {{ alert.error }}
+
+
+ Success!
+ {{ alert.success }}
+
+
+
+
@@ -159,13 +180,21 @@ const searchQuery = ref("");
const selectedCategory = ref(0);
const creatingItem = ref(false);
const deleting = ref(false);
-
+const alert = ref(null);
+const timer = ref(null);
const detail = ref({});
const showOverlay = ref(false);
const currentFotoIndex = ref(0);
-
const kategori = ref([]);
-const loading = ref(false); // 🔥 Loading persis kategori
+const loading = ref(false);
+
+function showAlert(type, message) {
+ alert.value = { [type]: message };
+ clearTimeout(timer.value);
+ timer.value = setTimeout(() => {
+ alert.value = null;
+ }, 5000);
+ }
// Load kategori
const loadKategori = async () => {
@@ -221,6 +250,18 @@ const closeItemModal = () => {
onMounted(async () => {
await loadKategori();
await loadProduk();
+
+ // 🔹 Cek apakah ada ?message= di URL
+ const params = new URLSearchParams(window.location.search);
+ const message = params.get("message");
+
+ if (message) {
+ showAlert("success", message);
+
+ // 🔹 Hapus query param biar tidak muncul lagi pas refresh
+ const newUrl = window.location.pathname;
+ window.history.replaceState({}, document.title, newUrl);
+ }
});
// Filter produk
diff --git a/resources/js/pages/Sales.vue b/resources/js/pages/Sales.vue
index 1c0264b..55fb3e0 100644
--- a/resources/js/pages/Sales.vue
+++ b/resources/js/pages/Sales.vue
@@ -1,190 +1,236 @@
-
-
-
+
+
+
-
+
-
-
+
+
-
-
SALES
-
-