[Refactor] Fix edit produk
This commit is contained in:
parent
ecc9385c28
commit
271a1e3660
29
.env.example
29
.env.example
@ -1,12 +1,12 @@
|
|||||||
APP_NAME=KasirTMJC
|
APP_NAME=Abbauf-Kasir
|
||||||
APP_ENV=local
|
APP_ENV=production
|
||||||
APP_KEY=
|
APP_KEY=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=false
|
||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
|
|
||||||
APP_LOCALE=id
|
APP_LOCALE=en
|
||||||
APP_FALLBACK_LOCALE=id
|
APP_FALLBACK_LOCALE=en
|
||||||
APP_FAKER_LOCALE=id_ID
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|
||||||
APP_MAINTENANCE_DRIVER=file
|
APP_MAINTENANCE_DRIVER=file
|
||||||
# APP_MAINTENANCE_STORE=database
|
# APP_MAINTENANCE_STORE=database
|
||||||
@ -20,15 +20,12 @@ LOG_STACK=single
|
|||||||
LOG_DEPRECATIONS_CHANNEL=null
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
DB_CONNECTION=mysql
|
DB_CONNECTION=sqlite
|
||||||
DB_HOST=mysql
|
# DB_HOST=127.0.0.1
|
||||||
DB_PORT=3306
|
# DB_PORT=3306
|
||||||
DB_DATABASE=kasir_db
|
# DB_DATABASE=laravel
|
||||||
DB_USERNAME=kasir_user
|
# DB_USERNAME=root
|
||||||
DB_PASSWORD=kasir_password
|
# DB_PASSWORD=
|
||||||
|
|
||||||
# MySQL Root Password (untuk Docker)
|
|
||||||
MYSQL_ROOT_PASSWORD=root_password
|
|
||||||
|
|
||||||
SESSION_DRIVER=database
|
SESSION_DRIVER=database
|
||||||
SESSION_LIFETIME=120
|
SESSION_LIFETIME=120
|
||||||
@ -46,7 +43,7 @@ CACHE_STORE=database
|
|||||||
MEMCACHED_HOST=127.0.0.1
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
REDIS_CLIENT=phpredis
|
REDIS_CLIENT=phpredis
|
||||||
REDIS_HOST=redis
|
REDIS_HOST=127.0.0.1
|
||||||
REDIS_PASSWORD=null
|
REDIS_PASSWORD=null
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
|||||||
91
Dockerfile
91
Dockerfile
@ -1,36 +1,91 @@
|
|||||||
# Stage 1: Build Vue (tetap sama)
|
# ========================================
|
||||||
FROM node:20 as node_builder
|
# Stage 1: Build Frontend Assets (Vue.js)
|
||||||
|
# ========================================
|
||||||
|
FROM node:20-alpine as node_builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files untuk caching layer
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm ci --legacy-peer-deps
|
||||||
|
|
||||||
|
# Copy seluruh source code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# Build production assets
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Stage 2: Laravel
|
# ========================================
|
||||||
FROM php:8.3-fpm
|
# Stage 2: Laravel Application
|
||||||
|
# ========================================
|
||||||
|
FROM php:8.3-fpm-alpine
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
# Install system dependencies dan PHP extensions
|
||||||
git unzip libzip-dev libpng-dev libonig-dev libxml2-dev curl \
|
RUN apk update && apk add --no-cache \
|
||||||
&& docker-php-ext-install pdo_mysql zip gd mbstring exif pcntl bcmath \
|
git \
|
||||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* # Cleanup untuk ukuran kecil
|
unzip \
|
||||||
|
libzip-dev \
|
||||||
|
libpng-dev \
|
||||||
|
oniguruma-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
curl \
|
||||||
|
mysql-client \
|
||||||
|
autoconf \
|
||||||
|
g++ \
|
||||||
|
make \
|
||||||
|
&& docker-php-ext-install \
|
||||||
|
pdo_mysql \
|
||||||
|
zip \
|
||||||
|
gd \
|
||||||
|
mbstring \
|
||||||
|
exif \
|
||||||
|
pcntl \
|
||||||
|
bcmath \
|
||||||
|
&& pecl install redis \
|
||||||
|
&& docker-php-ext-enable redis \
|
||||||
|
&& apk del autoconf g++ make \
|
||||||
|
&& rm -rf /var/cache/apk/* /tmp/*
|
||||||
|
|
||||||
|
# Install Composer
|
||||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
WORKDIR /var/www/html
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
# Copy source code
|
# Copy composer files untuk caching layer
|
||||||
|
COPY composer.json composer.lock ./
|
||||||
|
|
||||||
|
# Install PHP dependencies
|
||||||
|
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
|
||||||
|
|
||||||
|
# Copy application source code
|
||||||
COPY . .
|
COPY . .
|
||||||
# Copy hasil build Vue
|
|
||||||
|
# Copy hasil build Vue dari stage 1
|
||||||
COPY --from=node_builder /app/public/build /var/www/html/public/build
|
COPY --from=node_builder /app/public/build /var/www/html/public/build
|
||||||
|
|
||||||
RUN composer install --no-dev --optimize-autoloader
|
# Generate autoload files dengan optimasi
|
||||||
RUN php artisan storage:link || true
|
RUN composer dump-autoload --optimize --classmap-authoritative
|
||||||
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
|
# Create required directories dan set permissions
|
||||||
RUN chown -R www-data:www-data /var/www/html \
|
RUN mkdir -p \
|
||||||
&& chmod -R 755 /var/www/html/storage \
|
storage/framework/cache \
|
||||||
&& chmod -R 755 /var/www/html/bootstrap/cache
|
storage/framework/sessions \
|
||||||
|
storage/framework/views \
|
||||||
|
storage/logs \
|
||||||
|
bootstrap/cache \
|
||||||
|
&& chown -R www-data:www-data \
|
||||||
|
/var/www/html/storage \
|
||||||
|
/var/www/html/bootstrap/cache \
|
||||||
|
&& chmod -R 775 \
|
||||||
|
/var/www/html/storage \
|
||||||
|
/var/www/html/bootstrap/cache
|
||||||
|
|
||||||
|
# Switch ke user non-root untuk keamanan
|
||||||
|
USER www-data
|
||||||
|
|
||||||
EXPOSE 9000
|
EXPOSE 9000
|
||||||
|
|
||||||
CMD ["php-fpm"]
|
CMD ["php-fpm"]
|
||||||
|
|||||||
@ -1,58 +1,176 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
# ========================================
|
||||||
|
# Laravel PHP-FPM Application
|
||||||
|
# ========================================
|
||||||
laravel:
|
laravel:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: laravel_app_prod
|
container_name: abbauf_kasir_app
|
||||||
|
restart: unless-stopped
|
||||||
|
working_dir: /var/www/html
|
||||||
volumes:
|
volumes:
|
||||||
- storage_data:/var/www/html/storage
|
# Mount storage untuk uploads dan logs (persistent)
|
||||||
ports:
|
- ./storage:/var/www/html/storage
|
||||||
- "9000"
|
# Mount public build assets (read-only)
|
||||||
depends_on:
|
- ./public/build:/var/www/html/public/build:ro
|
||||||
- mysql
|
|
||||||
environment:
|
environment:
|
||||||
APP_ENV: production
|
# Application
|
||||||
APP_DEBUG: false
|
APP_NAME: ${APP_NAME:-Abbauf-Kasir}
|
||||||
|
APP_ENV: ${APP_ENV:-production}
|
||||||
|
APP_KEY: ${APP_KEY}
|
||||||
|
APP_DEBUG: ${APP_DEBUG:-false}
|
||||||
|
APP_URL: ${APP_URL:-http://localhost}
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DB_CONNECTION: mysql
|
||||||
|
DB_HOST: mysql
|
||||||
|
DB_PORT: 3306
|
||||||
|
DB_DATABASE: ${DB_DATABASE:-kasir_db}
|
||||||
|
DB_USERNAME: ${DB_USERNAME:-kasir_user}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
|
|
||||||
|
# Cache & Session
|
||||||
|
CACHE_STORE: redis
|
||||||
|
SESSION_DRIVER: redis
|
||||||
|
REDIS_HOST: redis
|
||||||
|
REDIS_PORT: 6379
|
||||||
|
|
||||||
|
# Queue
|
||||||
|
QUEUE_CONNECTION: redis
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_started
|
||||||
|
networks:
|
||||||
|
- kasir_network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "php-fpm", "-t"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Nginx Web Server
|
||||||
|
# ========================================
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: abbauf_kasir_nginx
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${APP_PORT:-80}:80"
|
||||||
|
volumes:
|
||||||
|
# Nginx configuration
|
||||||
|
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
# Laravel public directory (untuk static assets)
|
||||||
|
- ./public:/var/www/html/public:ro
|
||||||
|
# Storage symlink untuk file uploads
|
||||||
|
- ./storage/app/public:/var/www/html/public/storage:ro
|
||||||
|
depends_on:
|
||||||
|
- laravel
|
||||||
|
networks:
|
||||||
|
- kasir_network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# MySQL Database
|
||||||
|
# ========================================
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.4
|
||||||
|
container_name: abbauf_kasir_db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root_secret_password}
|
||||||
|
MYSQL_DATABASE: ${DB_DATABASE:-kasir_db}
|
||||||
|
MYSQL_USER: ${DB_USERNAME:-kasir_user}
|
||||||
|
MYSQL_PASSWORD: ${DB_PASSWORD}
|
||||||
|
MYSQL_CHARACTER_SET_SERVER: utf8mb4
|
||||||
|
MYSQL_COLLATION_SERVER: utf8mb4_unicode_ci
|
||||||
|
ports:
|
||||||
|
- "${DB_PORT:-3306}:3306"
|
||||||
|
volumes:
|
||||||
|
- mysql_data:/var/lib/mysql
|
||||||
|
# Optional: backup folder
|
||||||
|
- ./docker/mysql/backups:/backups
|
||||||
|
networks:
|
||||||
|
- kasir_network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-root_secret_password}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
command: --default-authentication-plugin=mysql_native_password
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Redis Cache & Session Store
|
||||||
|
# ========================================
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: abbauf_kasir_redis
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${REDIS_PORT:-6379}:6379"
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
networks:
|
||||||
|
- kasir_network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
command: redis-server --appendonly yes
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Queue Worker (Optional - untuk background jobs)
|
||||||
|
# ========================================
|
||||||
|
queue:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: abbauf_kasir_queue
|
||||||
|
restart: unless-stopped
|
||||||
|
working_dir: /var/www/html
|
||||||
|
volumes:
|
||||||
|
- ./storage:/var/www/html/storage
|
||||||
|
environment:
|
||||||
|
APP_ENV: ${APP_ENV:-production}
|
||||||
APP_KEY: ${APP_KEY}
|
APP_KEY: ${APP_KEY}
|
||||||
DB_CONNECTION: mysql
|
DB_CONNECTION: mysql
|
||||||
DB_HOST: mysql
|
DB_HOST: mysql
|
||||||
DB_PORT: 3306
|
DB_PORT: 3306
|
||||||
DB_DATABASE: ${DB_DATABASE}
|
DB_DATABASE: ${DB_DATABASE:-kasir_db}
|
||||||
DB_USERNAME: ${DB_USERNAME}
|
DB_USERNAME: ${DB_USERNAME:-kasir_user}
|
||||||
DB_PASSWORD: ${DB_PASSWORD}
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
|
REDIS_HOST: redis
|
||||||
nginx:
|
QUEUE_CONNECTION: redis
|
||||||
image: nginx:alpine
|
|
||||||
container_name: nginx_prod
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
volumes:
|
|
||||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
|
||||||
volumes_from:
|
|
||||||
- laravel:ro
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- laravel
|
- laravel
|
||||||
|
- redis
|
||||||
|
- mysql
|
||||||
|
networks:
|
||||||
|
- kasir_network
|
||||||
|
command: php artisan queue:work --tries=3 --timeout=90
|
||||||
|
|
||||||
mysql:
|
# ========================================
|
||||||
image: mysql:8
|
# Networks
|
||||||
container_name: mysql_db_prod
|
# ========================================
|
||||||
restart: unless-stopped
|
networks:
|
||||||
environment:
|
kasir_network:
|
||||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
driver: bridge
|
||||||
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"
|
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Persistent Volumes
|
||||||
|
# ========================================
|
||||||
volumes:
|
volumes:
|
||||||
mysql_data:
|
mysql_data:
|
||||||
storage_data:
|
driver: local
|
||||||
|
redis_data:
|
||||||
|
driver: local
|
||||||
|
|||||||
75
nginx.conf
75
nginx.conf
@ -1,14 +1,43 @@
|
|||||||
|
# ========================================
|
||||||
|
# Abbauf Kasir - Nginx Configuration
|
||||||
|
# ========================================
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
index index.php index.html;
|
listen [::]:80;
|
||||||
error_log /var/log/nginx/error.log;
|
server_name localhost;
|
||||||
access_log /var/log/nginx/access.log;
|
|
||||||
root /var/www/html/public;
|
|
||||||
|
|
||||||
|
root /var/www/html/public;
|
||||||
|
index index.php index.html index.htm;
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
access_log /var/log/nginx/access.log;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
|
||||||
|
# Security Headers
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
|
||||||
|
# Client body size limit (untuk upload file)
|
||||||
|
client_max_body_size 20M;
|
||||||
|
|
||||||
|
# Gzip compression
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types text/plain text/css text/xml text/javascript
|
||||||
|
application/json application/javascript application/xml+rss
|
||||||
|
application/rss+xml font/truetype font/opentype
|
||||||
|
application/vnd.ms-fontobject image/svg+xml;
|
||||||
|
|
||||||
|
# Main location block
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.php?$query_string;
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# PHP-FPM configuration
|
||||||
location ~ \.php$ {
|
location ~ \.php$ {
|
||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||||
@ -17,5 +46,43 @@ server {
|
|||||||
include fastcgi_params;
|
include fastcgi_params;
|
||||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||||
|
|
||||||
|
# Buffer settings untuk performa
|
||||||
|
fastcgi_buffer_size 128k;
|
||||||
|
fastcgi_buffers 256 16k;
|
||||||
|
fastcgi_busy_buffers_size 256k;
|
||||||
|
fastcgi_temp_file_write_size 256k;
|
||||||
|
|
||||||
|
# Timeout settings
|
||||||
|
fastcgi_read_timeout 300;
|
||||||
|
fastcgi_connect_timeout 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deny access to hidden files
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
access_log off;
|
||||||
|
log_not_found off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deny access to sensitive files
|
||||||
|
location ~ /(?:\.env|\.git|composer\.json|composer\.lock|package\.json|package-lock\.json|README\.md|\.gitignore) {
|
||||||
|
deny all;
|
||||||
|
access_log off;
|
||||||
|
log_not_found off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check endpoint
|
||||||
|
location /health {
|
||||||
|
access_log off;
|
||||||
|
return 200 "healthy\n";
|
||||||
|
add_header Content-Type text/plain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -56,21 +56,20 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<label class="block text-D mb-1">Harga per Gram</label>
|
<label class="block text-D mb-1">Harga per Gram</label>
|
||||||
<InputField
|
<InputField
|
||||||
v-model="hargaPerGramFormatted"
|
v-model="form.harga_per_gram"
|
||||||
type="text"
|
type="number"
|
||||||
|
step="0.01"
|
||||||
placeholder="Masukkan harga per gram"
|
placeholder="Masukkan harga per gram"
|
||||||
@input="formatHargaPerGramInput"
|
@input="calculateHargaJual"
|
||||||
@keypress="onlyNumbers"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<label class="block text-D mb-1">Harga Jual</label>
|
<label class="block text-D mb-1">Harga Jual</label>
|
||||||
<InputField
|
<InputField
|
||||||
v-model="hargaJualFormatted"
|
v-model="form.harga_jual"
|
||||||
type="text"
|
type="number"
|
||||||
|
step="0.01"
|
||||||
placeholder="Masukkan harga jual"
|
placeholder="Masukkan harga jual"
|
||||||
@input="formatHargaJualInput"
|
|
||||||
@keypress="onlyNumbers"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -80,7 +79,7 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<label class="block text-D mb-1">Foto</label>
|
<label class="block text-D mb-1">Foto</label>
|
||||||
|
|
||||||
<div class="grid grid-cols-3 gap-3">
|
<div class="grid grid-cols-3 gap-3 relative">
|
||||||
<!-- Existing Images -->
|
<!-- Existing Images -->
|
||||||
<div
|
<div
|
||||||
v-for="(image, index) in uploadedImages"
|
v-for="(image, index) in uploadedImages"
|
||||||
@ -109,24 +108,85 @@
|
|||||||
<!-- Upload Button -->
|
<!-- Upload Button -->
|
||||||
<div
|
<div
|
||||||
v-if="uploadedImages.length < 6"
|
v-if="uploadedImages.length < 6"
|
||||||
@drop="handleDrop"
|
class="relative aspect-square"
|
||||||
@dragover.prevent
|
|
||||||
@dragenter.prevent="isDragging = true"
|
|
||||||
@dragleave.prevent="isDragging = false"
|
|
||||||
@click="triggerFileInput"
|
|
||||||
class="aspect-square bg-gray-50 border-2 border-dashed border-gray-300 rounded-lg flex flex-col items-center justify-center cursor-pointer hover:border-D hover:bg-blue-50 transition-colors group"
|
|
||||||
:class="{
|
|
||||||
'border-blue-400 bg-blue-50': isDragging,
|
|
||||||
'cursor-not-allowed opacity-50': uploadLoading,
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<div class="text-center">
|
<div
|
||||||
<div
|
@drop="handleDrop"
|
||||||
v-if="!uploadLoading"
|
@dragover.prevent
|
||||||
class="w-12 h-12 bg-D rounded-lg flex items-center justify-center mx-auto mb-2 group-hover:bg-D transition-colors"
|
@dragenter.prevent="isDragging = true"
|
||||||
|
@dragleave.prevent="isDragging = false"
|
||||||
|
@click="toggleUploadMenu"
|
||||||
|
class="w-full h-full bg-gray-50 border-2 border-dashed border-gray-300 rounded-lg flex flex-col items-center justify-center cursor-pointer hover:border-D hover:bg-blue-50 transition-colors group"
|
||||||
|
:class="{
|
||||||
|
'border-blue-400 bg-blue-50': isDragging,
|
||||||
|
'cursor-not-allowed opacity-50': uploadLoading,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="text-center">
|
||||||
|
<div
|
||||||
|
v-if="!uploadLoading"
|
||||||
|
class="w-12 h-12 bg-D rounded-lg flex items-center justify-center mx-auto mb-2 group-hover:bg-D transition-colors"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6 text-white"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="w-12 h-12 bg-D rounded-lg flex items-center justify-center mx-auto mb-2"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="animate-spin w-6 h-6 text-white"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
class="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
class="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="text-xs text-gray-600 font-medium"
|
||||||
|
v-html="
|
||||||
|
uploadLoading
|
||||||
|
? 'Uploading...'
|
||||||
|
: 'Unggah<br/>Foto'
|
||||||
|
"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Menu -->
|
||||||
|
<div
|
||||||
|
v-if="showUploadMenu"
|
||||||
|
class="absolute top-full left-0 mt-2 w-60 bg-white border border-gray-200 rounded-lg shadow-lg z-20"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
@click="triggerFileUpload"
|
||||||
|
class="w-full px-4 py-3 text-left hover:bg-gray-50 flex items-center space-x-3 border-b border-gray-100"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
class="w-6 h-6 text-white"
|
class="w-5 h-5 text-gray-600"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -135,42 +195,50 @@
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
<div>
|
||||||
<div
|
<div class="font-medium text-gray-900">
|
||||||
v-else
|
Upload dari File
|
||||||
class="w-12 h-12 bg-D rounded-lg flex items-center justify-center mx-auto mb-2"
|
</div>
|
||||||
|
<div class="text-sm text-gray-500">
|
||||||
|
Pilih foto dari galeri
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="openCameraModal"
|
||||||
|
class="w-full px-4 py-3 text-left hover:bg-gray-50 flex items-center space-x-3"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
class="animate-spin w-6 h-6 text-white"
|
class="w-5 h-5 text-gray-600"
|
||||||
fill="none"
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<circle
|
|
||||||
class="opacity-25"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="10"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="4"
|
|
||||||
></circle>
|
|
||||||
<path
|
<path
|
||||||
class="opacity-75"
|
stroke-linecap="round"
|
||||||
fill="currentColor"
|
stroke-linejoin="round"
|
||||||
d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
stroke-width="2"
|
||||||
></path>
|
d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
<div>
|
||||||
<p
|
<div class="font-medium text-gray-900">
|
||||||
class="text-xs text-gray-600 font-medium"
|
Ambil dari Kamera
|
||||||
v-html="
|
</div>
|
||||||
uploadLoading
|
<div class="text-sm text-gray-500">
|
||||||
? 'Uploading...'
|
Foto langsung dengan kamera
|
||||||
: 'Unggah<br/>Foto'
|
</div>
|
||||||
"
|
</div>
|
||||||
></p>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -213,6 +281,43 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Overlay -->
|
||||||
|
<div
|
||||||
|
v-if="showUploadMenu"
|
||||||
|
@click="showUploadMenu = false"
|
||||||
|
class="fixed inset-0 z-10"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Camera Modal -->
|
||||||
|
<div
|
||||||
|
v-if="showCamera"
|
||||||
|
class="fixed inset-0 bg-black/75 flex items-center justify-center z-[9999]"
|
||||||
|
>
|
||||||
|
<div class="bg-white w-[480px] rounded-lg shadow-lg p-4 relative">
|
||||||
|
<video
|
||||||
|
ref="video"
|
||||||
|
autoplay
|
||||||
|
playsinline
|
||||||
|
class="w-full h-64 bg-black rounded"
|
||||||
|
></video>
|
||||||
|
<canvas ref="canvas" class="hidden"></canvas>
|
||||||
|
<div class="mt-3 flex justify-between">
|
||||||
|
<button
|
||||||
|
@click="closeCamera"
|
||||||
|
class="px-4 py-2 bg-gray-400 text-white rounded"
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="capturePhoto"
|
||||||
|
class="px-4 py-2 bg-blue-500 text-white rounded"
|
||||||
|
>
|
||||||
|
Ambil Foto
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</mainLayout>
|
</mainLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -235,8 +340,8 @@ const form = ref({
|
|||||||
id_kategori: null,
|
id_kategori: null,
|
||||||
berat: 0,
|
berat: 0,
|
||||||
kadar: 0,
|
kadar: 0,
|
||||||
harga_per_gram: null,
|
harga_per_gram: 0,
|
||||||
harga_jual: null,
|
harga_jual: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const category = ref([]);
|
const category = ref([]);
|
||||||
@ -246,66 +351,15 @@ const uploadLoading = ref(false);
|
|||||||
const uploadError = ref("");
|
const uploadError = ref("");
|
||||||
const isDragging = ref(false);
|
const isDragging = ref(false);
|
||||||
const fileInput = ref(null);
|
const fileInput = ref(null);
|
||||||
|
const showUploadMenu = ref(false);
|
||||||
|
const showCamera = ref(false);
|
||||||
|
const video = ref(null);
|
||||||
|
const canvas = ref(null);
|
||||||
|
let stream = null;
|
||||||
|
|
||||||
const openItemModal = ref(false);
|
const openItemModal = ref(false);
|
||||||
const editedProduct = ref(null);
|
const editedProduct = ref(null);
|
||||||
|
|
||||||
// Formatted values for harga_per_gram and harga_jual
|
|
||||||
const hargaPerGramFormatted = ref("");
|
|
||||||
const hargaJualFormatted = ref("");
|
|
||||||
|
|
||||||
// Format angka dengan pemisah ribuan
|
|
||||||
const formatNumber = (num) => {
|
|
||||||
if (!num) return "";
|
|
||||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
|
|
||||||
};
|
|
||||||
|
|
||||||
// Menghapus format dan mengambil angka asli
|
|
||||||
const unformatNumber = (str) => {
|
|
||||||
if (!str) return null;
|
|
||||||
const cleaned = str.replace(/\./g, "");
|
|
||||||
const number = parseFloat(cleaned);
|
|
||||||
return isNaN(number) ? null : number;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handler untuk format input harga per gram
|
|
||||||
const formatHargaPerGramInput = (event) => {
|
|
||||||
const value = event.target.value;
|
|
||||||
const cleanValue = value.replace(/\D/g, "");
|
|
||||||
if (cleanValue) {
|
|
||||||
const formatted = formatNumber(cleanValue);
|
|
||||||
hargaPerGramFormatted.value = formatted;
|
|
||||||
form.value.harga_per_gram = parseFloat(cleanValue);
|
|
||||||
calculateHargaJual();
|
|
||||||
} else {
|
|
||||||
hargaPerGramFormatted.value = "";
|
|
||||||
form.value.harga_per_gram = null;
|
|
||||||
calculateHargaJual();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handler untuk format input harga jual
|
|
||||||
const formatHargaJualInput = (event) => {
|
|
||||||
const value = event.target.value;
|
|
||||||
const cleanValue = value.replace(/\D/g, "");
|
|
||||||
if (cleanValue) {
|
|
||||||
const formatted = formatNumber(cleanValue);
|
|
||||||
hargaJualFormatted.value = formatted;
|
|
||||||
form.value.harga_jual = parseFloat(cleanValue);
|
|
||||||
} else {
|
|
||||||
hargaJualFormatted.value = "";
|
|
||||||
form.value.harga_jual = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Hanya izinkan angka saat mengetik
|
|
||||||
const onlyNumbers = (event) => {
|
|
||||||
const char = String.fromCharCode(event.which);
|
|
||||||
if (!/[0-9]/.test(char)) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isFormValid = computed(() => {
|
const isFormValid = computed(() => {
|
||||||
return (
|
return (
|
||||||
form.value.nama &&
|
form.value.nama &&
|
||||||
@ -321,42 +375,42 @@ const calculateHargaJual = () => {
|
|||||||
const berat = parseFloat(form.value.berat) || 0;
|
const berat = parseFloat(form.value.berat) || 0;
|
||||||
const hargaPerGram = parseFloat(form.value.harga_per_gram) || 0;
|
const hargaPerGram = parseFloat(form.value.harga_per_gram) || 0;
|
||||||
if (berat > 0 && hargaPerGram > 0) {
|
if (berat > 0 && hargaPerGram > 0) {
|
||||||
const hargaJual = berat * hargaPerGram;
|
form.value.harga_jual = berat * hargaPerGram;
|
||||||
form.value.harga_jual = hargaJual;
|
|
||||||
hargaJualFormatted.value = formatNumber(hargaJual.toFixed(0));
|
|
||||||
} else {
|
|
||||||
form.value.harga_jual = null;
|
|
||||||
hargaJualFormatted.value = "";
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadKategori = async () => {
|
const loadKategori = async () => {
|
||||||
const response = await axios.get("/api/kategori", {
|
try {
|
||||||
headers: {
|
const response = await axios.get("/api/kategori", {
|
||||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
headers: {
|
||||||
},
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
});
|
},
|
||||||
category.value = response.data.map((c) => ({ value: c.id, label: c.nama }));
|
});
|
||||||
|
category.value = response.data.map((c) => ({ value: c.id, label: c.nama }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading categories:", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadProduk = async () => {
|
const loadProduk = async () => {
|
||||||
const response = await axios.get(`/api/produk/edit/${productId}`, {
|
try {
|
||||||
headers: {
|
const response = await axios.get(`/api/produk/edit/${productId}`, {
|
||||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
headers: {
|
||||||
},
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
});
|
},
|
||||||
const produk = response.data;
|
});
|
||||||
form.value = {
|
const produk = response.data;
|
||||||
nama: produk.nama,
|
form.value = {
|
||||||
id_kategori: produk.id_kategori,
|
nama: produk.nama,
|
||||||
berat: produk.berat,
|
id_kategori: produk.id_kategori,
|
||||||
kadar: produk.kadar,
|
berat: produk.berat,
|
||||||
harga_per_gram: produk.harga_per_gram,
|
kadar: produk.kadar,
|
||||||
harga_jual: produk.harga_jual,
|
harga_per_gram: produk.harga_per_gram,
|
||||||
};
|
harga_jual: produk.harga_jual,
|
||||||
// Set formatted values after loading
|
};
|
||||||
hargaPerGramFormatted.value = formatNumber(produk.harga_per_gram?.toString() || "");
|
} catch (error) {
|
||||||
hargaJualFormatted.value = formatNumber(produk.harga_jual?.toString() || "");
|
console.error("Error loading product:", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadFoto = async () => {
|
const loadFoto = async () => {
|
||||||
@ -367,18 +421,23 @@ const loadFoto = async () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
uploadedImages.value = response.data;
|
uploadedImages.value = response.data;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
console.error(e);
|
console.error("Error loading photos:", error);
|
||||||
uploadError.value = "Gagal memuat foto";
|
uploadError.value = "Gagal memuat foto";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const triggerFileInput = () => {
|
const toggleUploadMenu = () => {
|
||||||
if (!uploadLoading.value && uploadedImages.value.length < 6) {
|
if (!uploadLoading.value && uploadedImages.value.length < 6) {
|
||||||
fileInput.value?.click();
|
showUploadMenu.value = !showUploadMenu.value;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const triggerFileUpload = () => {
|
||||||
|
showUploadMenu.value = false;
|
||||||
|
fileInput.value?.click();
|
||||||
|
};
|
||||||
|
|
||||||
const handleFileSelect = (e) => {
|
const handleFileSelect = (e) => {
|
||||||
const files = Array.from(e.target.files);
|
const files = Array.from(e.target.files);
|
||||||
uploadFiles(files);
|
uploadFiles(files);
|
||||||
@ -393,60 +452,47 @@ const handleDrop = (e) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const uploadFiles = async (files) => {
|
const uploadFiles = async (files) => {
|
||||||
uploadError.value = '';
|
uploadError.value = '';
|
||||||
|
if (uploadedImages.value.length + files.length > 6) {
|
||||||
if (uploadedImages.value.length + files.length > 6) {
|
uploadError.value = 'Maksimal 6 foto yang dapat diupload';
|
||||||
uploadError.value = 'Maksimal 6 foto yang dapat diupload';
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validFiles = files.filter(file => {
|
|
||||||
const isValidType = ['image/jpeg', 'image/jpg', 'image/png'].includes(file.type);
|
|
||||||
const isValidSize = file.size <= 2 * 1024 * 1024;
|
|
||||||
|
|
||||||
if (!isValidType) {
|
|
||||||
uploadError.value = 'Format file harus JPG, JPEG, atau PNG';
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
const validFiles = files.filter(file => {
|
||||||
if (!isValidSize) {
|
const isValidType = ['image/jpeg', 'image/jpg', 'image/png'].includes(file.type);
|
||||||
uploadError.value = 'Ukuran file maksimal 2MB';
|
const isValidSize = file.size <= 2 * 1024 * 1024;
|
||||||
return false;
|
if (!isValidType) {
|
||||||
|
uploadError.value = 'Format file harus JPG, JPEG, atau PNG';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!isValidSize) {
|
||||||
|
uploadError.value = 'Ukuran file maksimal 2MB';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (validFiles.length === 0) return;
|
||||||
|
uploadLoading.value = true;
|
||||||
|
try {
|
||||||
|
for (const file of validFiles) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('foto', file);
|
||||||
|
const response = await axios.post('/api/foto', formData, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
uploadedImages.value.push(response.data);
|
||||||
|
}
|
||||||
|
if (fileInput.value) {
|
||||||
|
fileInput.value.value = '';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload error:', error);
|
||||||
|
uploadError.value = error.response?.data?.message || 'Gagal mengupload foto';
|
||||||
|
} finally {
|
||||||
|
uploadLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (validFiles.length === 0) return;
|
|
||||||
|
|
||||||
uploadLoading.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const file of validFiles) {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('foto', file);
|
|
||||||
|
|
||||||
const response = await axios.post('/api/foto', formData, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
uploadedImages.value.push(response.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileInput.value) {
|
|
||||||
fileInput.value.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Upload error:', error);
|
|
||||||
uploadError.value = error.response?.data?.message || 'Gagal mengupload foto';
|
|
||||||
} finally {
|
|
||||||
uploadLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeImage = async (id) => {
|
const removeImage = async (id) => {
|
||||||
@ -462,21 +508,51 @@ const removeImage = async (id) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openCameraModal = async () => {
|
||||||
|
showUploadMenu.value = false;
|
||||||
|
showCamera.value = true;
|
||||||
|
try {
|
||||||
|
stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
||||||
|
video.value.srcObject = stream;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal akses kamera:", err);
|
||||||
|
alert("Tidak bisa mengakses kamera, cek izin browser!");
|
||||||
|
closeCamera();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeCamera = () => {
|
||||||
|
showCamera.value = false;
|
||||||
|
if (stream) {
|
||||||
|
stream.getTracks().forEach(track => track.stop());
|
||||||
|
stream = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const capturePhoto = () => {
|
||||||
|
const ctx = canvas.value.getContext("2d");
|
||||||
|
canvas.value.width = video.value.videoWidth;
|
||||||
|
canvas.value.height = video.value.videoHeight;
|
||||||
|
ctx.drawImage(video.value, 0, 0);
|
||||||
|
canvas.value.toBlob(async (blob) => {
|
||||||
|
if (!blob) return;
|
||||||
|
await uploadFiles([new File([blob], "camera_photo.png", { type: "image/png" })]);
|
||||||
|
closeCamera();
|
||||||
|
}, "image/png");
|
||||||
|
};
|
||||||
|
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
await axios.put(
|
await axios.put(`/api/produk/${productId}`, form.value, {
|
||||||
`/api/produk/${productId}`,form.value,
|
headers: {
|
||||||
{
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
headers: {
|
},
|
||||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
});
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
router.push("/produk?message=Produk berhasil diperbarui");
|
router.push("/produk?message=Produk berhasil diperbarui");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error("Submit error:", err);
|
||||||
uploadError.value = err.response?.data?.message || "Gagal menyimpan produk";
|
uploadError.value = err.response?.data?.message || "Gagal menyimpan produk";
|
||||||
console.error(err);
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@ -484,6 +560,7 @@ const submitForm = async () => {
|
|||||||
|
|
||||||
const closeItemModal = () => {
|
const closeItemModal = () => {
|
||||||
openItemModal.value = false;
|
openItemModal.value = false;
|
||||||
|
editedProduct.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const back = () => {
|
const back = () => {
|
||||||
@ -493,7 +570,6 @@ const back = () => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadKategori();
|
await loadKategori();
|
||||||
await loadProduk();
|
await loadProduk();
|
||||||
loadFoto();
|
await loadFoto();
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user