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 - + +
+ + +
+
{ + 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
- + +
+ + +
+
@@ -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 @@