Merge branch 'production' of https://git.abbauf.com/Magang-2025/Kasir into production
This commit is contained in:
commit
7766fd8938
51
app/Http/Controllers/AuthController.php
Normal file
51
app/Http/Controllers/AuthController.php
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
|
class AuthController extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function login(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'nama' => 'required',
|
||||||
|
'password' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// cari user berdasarkan nama
|
||||||
|
$user = User::where('nama', $request->nama)->first();
|
||||||
|
|
||||||
|
if (!$user || !Hash::check($request->password, $user->password)) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Nama atau password salah'
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// buat token Sanctum
|
||||||
|
$token = $user->createToken('auth_token')->plainTextToken;
|
||||||
|
|
||||||
|
$redirectUrl = $user->role === 'owner' ? '/brankas' : '/kasir';
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Login berhasil',
|
||||||
|
'user' => $user,
|
||||||
|
'token' => $token,
|
||||||
|
'redirect' => $redirectUrl,
|
||||||
|
'role' => $user->role
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logout(Request $request)
|
||||||
|
{
|
||||||
|
$request->user()->currentAccessToken()->delete();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Logout berhasil'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
276
app/Http/Controllers/LaporanController.php
Normal file
276
app/Http/Controllers/LaporanController.php
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\ItemTransaksi;
|
||||||
|
use App\Models\Produk;
|
||||||
|
use App\Models\Transaksi;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Carbon\CarbonPeriod;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class LaporanController extends Controller
|
||||||
|
{
|
||||||
|
public function ringkasan(Request $request)
|
||||||
|
{
|
||||||
|
$filter = $request->query('filter', 'bulan');
|
||||||
|
$page = $request->query('page', 1);
|
||||||
|
|
||||||
|
$allSalesNames = Transaksi::select('nama_sales')->distinct()->pluck('nama_sales');
|
||||||
|
|
||||||
|
if ($filter === 'hari') {
|
||||||
|
return $this->laporanHarian($page, $allSalesNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->laporanBulanan($page, $allSalesNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function laporanHarian(int $page, Collection $allSalesNames)
|
||||||
|
{
|
||||||
|
$perPage = 7;
|
||||||
|
|
||||||
|
$endDate = Carbon::today()->subDays(($page - 1) * $perPage);
|
||||||
|
$startDate = $endDate->copy()->subDays($perPage - 1);
|
||||||
|
|
||||||
|
$transaksis = Transaksi::with('itemTransaksi.item.produk')
|
||||||
|
->whereBetween('created_at', [$startDate->startOfDay(), $endDate->endOfDay()])
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$transaksisByDay = $transaksis->groupBy(function ($transaksi) {
|
||||||
|
return Carbon::parse($transaksi->created_at)->format('Y-m-d');
|
||||||
|
});
|
||||||
|
|
||||||
|
$period = CarbonPeriod::create($startDate, $endDate);
|
||||||
|
$laporan = [];
|
||||||
|
|
||||||
|
foreach ($period as $date) {
|
||||||
|
$dateString = $date->format('Y-m-d');
|
||||||
|
$tanggalFormatted = $date->isoFormat('dddd, D MMMM Y');
|
||||||
|
|
||||||
|
if (isset($transaksisByDay[$dateString])) {
|
||||||
|
$transaksisPerTanggal = $transaksisByDay[$dateString];
|
||||||
|
|
||||||
|
$salesDataTransaksi = $transaksisPerTanggal->groupBy('nama_sales')
|
||||||
|
->map(fn($transaksisPerSales) => $this->hitungDataSales($transaksisPerSales));
|
||||||
|
|
||||||
|
$fullSalesData = $allSalesNames->map(function ($namaSales) use ($salesDataTransaksi) {
|
||||||
|
return $salesDataTransaksi->get($namaSales) ?? $this->defaultSalesData($namaSales);
|
||||||
|
});
|
||||||
|
|
||||||
|
$totalItem = $fullSalesData->sum('item_terjual');
|
||||||
|
$totalBerat = $fullSalesData->sum('berat_terjual_raw');
|
||||||
|
$totalPendapatan = $fullSalesData->sum('pendapatan_raw');
|
||||||
|
|
||||||
|
$laporan[$dateString] = [
|
||||||
|
'tanggal' => $tanggalFormatted,
|
||||||
|
'total_item_terjual' => $totalItem > 0 ? $totalItem : '-',
|
||||||
|
'total_berat' => $totalBerat > 0 ? number_format($totalBerat, 2, ',', '.') . 'g' : '-',
|
||||||
|
'total_pendapatan' => $totalPendapatan > 0 ? 'Rp' . number_format($totalPendapatan, 2, ',', '.') : '-',
|
||||||
|
'sales' => $this->formatSalesDataValues($fullSalesData)->values(),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$laporan[$dateString] = [
|
||||||
|
'tanggal' => $tanggalFormatted,
|
||||||
|
'total_item_terjual' => '-',
|
||||||
|
'total_berat' => '-',
|
||||||
|
'total_pendapatan' => '-',
|
||||||
|
'sales' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$totalHariUntukPaginasi = 365;
|
||||||
|
$paginatedData = new LengthAwarePaginator(
|
||||||
|
array_reverse(array_values($laporan)),
|
||||||
|
$totalHariUntukPaginasi,
|
||||||
|
$perPage,
|
||||||
|
$page,
|
||||||
|
['path' => request()->url(), 'query' => request()->query()]
|
||||||
|
);
|
||||||
|
|
||||||
|
return response()->json($paginatedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function laporanBulanan(int $page, Collection $allSalesNames)
|
||||||
|
{
|
||||||
|
$perPage = 12;
|
||||||
|
|
||||||
|
$transaksis = Transaksi::with('itemTransaksi.item.produk')
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$laporan = $transaksis->groupBy(function ($transaksi) {
|
||||||
|
return Carbon::parse($transaksi->created_at)->format('F Y');
|
||||||
|
})
|
||||||
|
->map(function ($transaksisPerTanggal, $tanggal) use ($allSalesNames) {
|
||||||
|
|
||||||
|
$salesDataTransaksi = $transaksisPerTanggal
|
||||||
|
->groupBy('nama_sales')
|
||||||
|
->map(fn($transaksisPerSales) => $this->hitungDataSales($transaksisPerSales));
|
||||||
|
|
||||||
|
$fullSalesData = $allSalesNames->map(function ($namaSales) use ($salesDataTransaksi) {
|
||||||
|
return $salesDataTransaksi->get($namaSales) ?? $this->defaultSalesData($namaSales);
|
||||||
|
});
|
||||||
|
|
||||||
|
$totalItem = $fullSalesData->sum('item_terjual');
|
||||||
|
$totalBerat = $fullSalesData->sum('berat_terjual_raw');
|
||||||
|
$totalPendapatan = $fullSalesData->sum('pendapatan_raw');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'tanggal' => $tanggal,
|
||||||
|
'total_item_terjual' => $totalItem > 0 ? $totalItem : '-',
|
||||||
|
'total_berat' => $totalBerat > 0 ? number_format($totalBerat, 2, ',', '.') . 'g' : '-',
|
||||||
|
'total_pendapatan' => $totalPendapatan > 0 ? 'Rp' . number_format($totalPendapatan, 2, ',', '.') : '-',
|
||||||
|
'sales' => $this->formatSalesDataValues($fullSalesData)->values(),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$paginatedData = new LengthAwarePaginator(
|
||||||
|
$laporan->forPage($page, $perPage)->values(),
|
||||||
|
$laporan->count(),
|
||||||
|
$perPage,
|
||||||
|
$page,
|
||||||
|
['path' => request()->url(), 'query' => request()->query()]
|
||||||
|
);
|
||||||
|
|
||||||
|
return response()->json($paginatedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hitungDataSales(Collection $transaksisPerSales): array
|
||||||
|
{
|
||||||
|
$itemTerjual = $transaksisPerSales->sum(fn($t) => $t->itemTransaksi->count());
|
||||||
|
$beratTerjual = $transaksisPerSales->sum(
|
||||||
|
fn($t) =>
|
||||||
|
$t->itemTransaksi->sum(fn($it) => $it->item->produk->berat ?? 0)
|
||||||
|
);
|
||||||
|
$pendapatan = $transaksisPerSales->sum('total_harga');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'nama' => $transaksisPerSales->first()->nama_sales,
|
||||||
|
'item_terjual' => $itemTerjual,
|
||||||
|
'berat_terjual_raw' => $beratTerjual,
|
||||||
|
'pendapatan_raw' => $pendapatan,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function defaultSalesData(string $namaSales): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'nama' => $namaSales,
|
||||||
|
'item_terjual' => 0,
|
||||||
|
'berat_terjual_raw' => 0,
|
||||||
|
'pendapatan_raw' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatSalesDataValues(Collection $salesData): Collection
|
||||||
|
{
|
||||||
|
return $salesData->map(function ($sale) {
|
||||||
|
$sale['item_terjual'] = $sale['item_terjual'] > 0 ? $sale['item_terjual'] : '-';
|
||||||
|
$sale['berat_terjual'] = $sale['berat_terjual_raw'] > 0 ? number_format($sale['berat_terjual_raw'], 2, ',', '.') . 'g' : '-';
|
||||||
|
$sale['pendapatan'] = $sale['pendapatan_raw'] > 0 ? 'Rp' . number_format($sale['pendapatan_raw'], 2, ',', '.') : '-';
|
||||||
|
|
||||||
|
unset($sale['berat_terjual_raw'], $sale['pendapatan_raw']);
|
||||||
|
return $sale;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function detail(Request $request)
|
||||||
|
{
|
||||||
|
// 1. VALIDASI DAN PENGAMBILAN PARAMETER FILTER
|
||||||
|
$request->validate([
|
||||||
|
'tanggal' => 'required|date_format:Y-m-d',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tanggal = $request->query('tanggal');
|
||||||
|
$namaSales = $request->query('nama_sales');
|
||||||
|
$posisi = $request->query('posisi');
|
||||||
|
$namaPembeli = $request->query('nama_pembeli'); // Untuk pencarian
|
||||||
|
|
||||||
|
$carbonDate = Carbon::parse($tanggal);
|
||||||
|
|
||||||
|
// 2. QUERY UTAMA UNTUK MENGAMBIL DATA PRODUK YANG TERJUAL BERDASARKAN FILTER
|
||||||
|
// Query ini hanya akan mengambil produk yang memiliki transaksi sesuai filter.
|
||||||
|
$produkTerjualQuery = ItemTransaksi::query()
|
||||||
|
->join('items', 'item_transaksis.id_item', '=', 'items.id')
|
||||||
|
->join('produks', 'items.id_produk', '=', 'produks.id')
|
||||||
|
->join('transaksis', 'item_transaksis.id_transaksi', '=', 'transaksis.id')
|
||||||
|
// Filter Wajib: Tanggal
|
||||||
|
->whereDate('transaksis.created_at', $carbonDate)
|
||||||
|
// Filter Opsional: Nama Sales
|
||||||
|
->when($namaSales, function ($query, $namaSales) {
|
||||||
|
return $query->where('transaksis.nama_sales', $namaSales);
|
||||||
|
})
|
||||||
|
// Filter Opsional: Posisi Asal Item
|
||||||
|
->when($posisi, function ($query, $posisi) {
|
||||||
|
return $query->where('item_transaksis.posisi_asal', $posisi);
|
||||||
|
})
|
||||||
|
// Filter Opsional: Nama Pembeli (menggunakan LIKE untuk pencarian)
|
||||||
|
->when($namaPembeli, function ($query, $namaPembeli) {
|
||||||
|
return $query->where('transaksis.nama_pembeli', 'like', "%{$namaPembeli}%");
|
||||||
|
})
|
||||||
|
->select(
|
||||||
|
'produks.id as id_produk',
|
||||||
|
'produks.nama as nama_produk',
|
||||||
|
DB::raw('COUNT(item_transaksis.id) as jumlah_item_terjual'),
|
||||||
|
DB::raw('SUM(produks.berat) as berat_terjual'),
|
||||||
|
DB::raw('SUM(item_transaksis.harga_deal) as pendapatan')
|
||||||
|
)
|
||||||
|
->groupBy('produks.id', 'produks.nama')
|
||||||
|
->get()
|
||||||
|
// Mengubah collection menjadi array asosiatif dengan key id_produk agar mudah dicari
|
||||||
|
->keyBy('id_produk');
|
||||||
|
|
||||||
|
|
||||||
|
// 3. MENGAMBIL SEMUA PRODUK DARI DATABASE
|
||||||
|
$semuaProduk = Produk::query()->select('id', 'nama')->get();
|
||||||
|
|
||||||
|
// 4. MENGGABUNGKAN DATA SEMUA PRODUK DENGAN PRODUK YANG TERJUAL
|
||||||
|
$detailItem = $semuaProduk->map(function ($produk) use ($produkTerjualQuery) {
|
||||||
|
// Cek apakah produk ini ada di dalam daftar produk yang terjual
|
||||||
|
if ($produkTerjualQuery->has($produk->id)) {
|
||||||
|
$dataTerjual = $produkTerjualQuery->get($produk->id);
|
||||||
|
return [
|
||||||
|
'nama_produk' => $produk->nama,
|
||||||
|
'jumlah_item_terjual' => (int) $dataTerjual->jumlah_item_terjual,
|
||||||
|
'berat_terjual' => (float) $dataTerjual->berat_terjual,
|
||||||
|
'pendapatan' => (float) $dataTerjual->pendapatan,
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// Jika produk tidak terjual, berikan nilai default "-"
|
||||||
|
return [
|
||||||
|
'nama_produk' => $produk->nama,
|
||||||
|
'jumlah_item_terjual' => '-',
|
||||||
|
'berat_terjual' => '-',
|
||||||
|
'pendapatan' => '-',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. MENGHITUNG TOTAL REKAP HARIAN DARI DATA YANG SUDAH DIFILTER
|
||||||
|
$totalPendapatan = $produkTerjualQuery->sum('pendapatan');
|
||||||
|
$totalItemTerjual = $produkTerjualQuery->sum('jumlah_item_terjual');
|
||||||
|
$totalBeratTerjual = $produkTerjualQuery->sum('berat_terjual');
|
||||||
|
|
||||||
|
// 6. MENYUSUN STRUKTUR RESPONSE FINAL
|
||||||
|
$response = [
|
||||||
|
'filter' => [
|
||||||
|
'tanggal' => $carbonDate->isoFormat('dddd, D MMMM Y'),
|
||||||
|
'nama_sales' => $namaSales,
|
||||||
|
'posisi' => $posisi,
|
||||||
|
'nama_pembeli' => $namaPembeli,
|
||||||
|
],
|
||||||
|
'rekap_harian' => [
|
||||||
|
'total_item_terjual' => $totalItemTerjual,
|
||||||
|
'total_berat_terjual' => $totalBeratTerjual,
|
||||||
|
'total_pendapatan' => $totalPendapatan,
|
||||||
|
],
|
||||||
|
'produk' => $detailItem,
|
||||||
|
];
|
||||||
|
|
||||||
|
return response()->json($response);
|
||||||
|
}
|
||||||
|
}
|
@ -26,7 +26,7 @@ class UserController extends Controller
|
|||||||
|
|
||||||
User::create([
|
User::create([
|
||||||
'nama' => $request->nama,
|
'nama' => $request->nama,
|
||||||
'password' => bcrypt($request->password),
|
'password' => $request->password,
|
||||||
'role' => $request->role,
|
'role' => $request->role,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -41,22 +41,26 @@ class UserController extends Controller
|
|||||||
$user = User::findOrFail($id);
|
$user = User::findOrFail($id);
|
||||||
|
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'nama' => 'required|nama|unique:users,nama,' . $id,
|
'nama' => 'required|string|unique:users,nama,' . $id,
|
||||||
'password' => 'required|min:6',
|
'password' => 'nullable|min:6',
|
||||||
'role' => 'required|in:owner,kasir',
|
'role' => 'required|in:owner,kasir',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user->update([
|
$data = [
|
||||||
'nama' => $request->nama,
|
'nama' => $request->nama,
|
||||||
'password' => $request->password,
|
|
||||||
'role' => $request->role,
|
'role' => $request->role,
|
||||||
]);
|
];
|
||||||
|
|
||||||
return response()->json([
|
if ($request->filled('password')) {
|
||||||
'message' => 'User berhasil diupdate'
|
$data['password'] = $request->password;
|
||||||
],200);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$user->update($data);
|
||||||
|
|
||||||
|
return response()->json(['message' => 'User berhasil diupdate', 'user' => $user], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public function destroy($id)
|
public function destroy($id)
|
||||||
{
|
{
|
||||||
$user = User::findOrFail($id);
|
$user = User::findOrFail($id);
|
||||||
|
30
app/Http/Middleware/RoleMiddleware.php
Normal file
30
app/Http/Middleware/RoleMiddleware.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class RoleMiddleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next, ...$roles): Response
|
||||||
|
{
|
||||||
|
// cek apakah user login
|
||||||
|
if (!$request->user()) {
|
||||||
|
return response()->json(['message' => 'Unauthenticated'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// cek role user
|
||||||
|
if (!in_array($request->user()->role, $roles)) {
|
||||||
|
return response()->json(['message' => 'Forbidden'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,8 @@ class ItemTransaksi extends Model
|
|||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'id_transaksi',
|
'id_transaksi',
|
||||||
'id_item',
|
'id_item',
|
||||||
'harga_deal'
|
'harga_deal',
|
||||||
|
'posisi_asal'
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $hidden = ['created_at', 'updated_at', 'deleted_at'];
|
protected $hidden = ['created_at', 'updated_at', 'deleted_at'];
|
||||||
|
@ -37,14 +37,4 @@ class Transaksi extends Model
|
|||||||
{
|
{
|
||||||
return $this->hasMany(ItemTransaksi::class, 'id_transaksi');
|
return $this->hasMany(ItemTransaksi::class, 'id_transaksi');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function items()
|
|
||||||
{
|
|
||||||
return $this->hasMany(ItemTransaksi::class, 'id_transaksi');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function foto ()
|
|
||||||
{
|
|
||||||
return $this->hasMany(Foto::class, 'id_produk');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -6,11 +6,13 @@ namespace App\Models;
|
|||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
|
||||||
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
use HasFactory, Notifiable;
|
use HasApiTokens, HasFactory, Notifiable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
@ -45,4 +47,9 @@ class User extends Authenticatable
|
|||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getAuthIdentifierName()
|
||||||
|
{
|
||||||
|
return 'id';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,10 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
$middleware->validateCsrfTokens(except: [
|
$middleware->validateCsrfTokens(except: [
|
||||||
'api/*'
|
'api/*'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$middleware->alias([
|
||||||
|
'role' => \App\Http\Middleware\RoleMiddleware::class,
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions): void {
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
//
|
//
|
||||||
|
4
composer.lock
generated
4
composer.lock
generated
@ -9377,12 +9377,12 @@
|
|||||||
],
|
],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"stability-flags": {},
|
"stability-flags": [],
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": {
|
"platform": {
|
||||||
"php": "^8.2"
|
"php": "^8.2"
|
||||||
},
|
},
|
||||||
"platform-dev": {},
|
"platform-dev": [],
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.6.0"
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,11 @@ return [
|
|||||||
'driver' => 'session',
|
'driver' => 'session',
|
||||||
'provider' => 'users',
|
'provider' => 'users',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'api' => [
|
||||||
|
'driver' => 'sanctum',
|
||||||
|
'provider' => 'users',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -17,12 +17,16 @@ class ProdukFactory extends Factory
|
|||||||
*/
|
*/
|
||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
|
$kategori = Kategori::inRandomOrder()->first();
|
||||||
|
|
||||||
$harga_per_gram = $this->faker->numberBetween(80, 120) * 10000;
|
$harga_per_gram = $this->faker->numberBetween(80, 120) * 10000;
|
||||||
$berat = $this->faker->randomFloat(2, 1, 10);
|
$berat = $this->faker->randomFloat(2, 1, 10);
|
||||||
$kategoriList = Kategori::all()->pluck('id')->toArray();
|
|
||||||
return [
|
return [
|
||||||
'nama' => $this->faker->words(3, true),
|
'nama' => $kategori->nama . ' ' . $this->faker->words(mt_rand(1, 2), true),
|
||||||
'id_kategori' => $this->faker->randomElement($kategoriList),
|
|
||||||
|
'id_kategori' => $kategori->id,
|
||||||
|
|
||||||
'berat' => $berat,
|
'berat' => $berat,
|
||||||
'kadar' => $this->faker->numberBetween(10, 24),
|
'kadar' => $this->faker->numberBetween(10, 24),
|
||||||
'harga_per_gram' => $harga_per_gram,
|
'harga_per_gram' => $harga_per_gram,
|
||||||
|
@ -22,16 +22,19 @@ class TransaksiFactory extends Factory
|
|||||||
$sales = Sales::inRandomOrder()->first();
|
$sales = Sales::inRandomOrder()->first();
|
||||||
$kasir = User::inRandomOrder()->first();
|
$kasir = User::inRandomOrder()->first();
|
||||||
|
|
||||||
|
$date = $this->faker->dateTimeBetween('-3 months');
|
||||||
|
$ongkos_bikin = $this->faker->numberBetween(8, 12) * 10000;
|
||||||
return [
|
return [
|
||||||
'id_kasir' => $kasir?->id,
|
'id_kasir' => $kasir?->id,
|
||||||
'id_sales' => $sales?->id,
|
'id_sales' => $sales?->id,
|
||||||
'nama_sales' => $sales?->nama ?? $this->faker->name(),
|
'nama_sales' => $sales?->nama,
|
||||||
'nama_pembeli' => $sales?->nama ?? $this->faker->name(),
|
'nama_pembeli' => $this->faker->name(),
|
||||||
'no_hp' => $this->faker->phoneNumber(),
|
'no_hp' => $this->faker->phoneNumber(),
|
||||||
'alamat' => $this->faker->address(),
|
'alamat' => $this->faker->address(),
|
||||||
'ongkos_bikin' => $this->faker->randomFloat(2, 0, 1000000),
|
'ongkos_bikin' => $ongkos_bikin,
|
||||||
'total_harga' => $this->faker->randomFloat(2, 100000, 5000000),
|
'total_harga' => $ongkos_bikin,
|
||||||
'created_at' => now(),
|
'created_at' => $date,
|
||||||
|
'updated_at' => $date,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ return new class extends Migration
|
|||||||
$table->foreignId('id_transaksi')->constrained('transaksis')->onDelete('cascade');
|
$table->foreignId('id_transaksi')->constrained('transaksis')->onDelete('cascade');
|
||||||
$table->foreignId('id_item')->constrained('items');
|
$table->foreignId('id_item')->constrained('items');
|
||||||
$table->double('harga_deal');
|
$table->double('harga_deal');
|
||||||
|
$table->string('posisi_asal', 100);
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -20,9 +20,14 @@ class DatabaseSeeder extends Seeder
|
|||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
User::factory()->create([
|
User::factory()->create([
|
||||||
'nama' => 'Test User',
|
'nama' => 'Owner',
|
||||||
'role' => 'owner',
|
'role' => 'owner',
|
||||||
'password' => bcrypt('123123123'),
|
'password' => bcrypt('123123'),
|
||||||
|
]);
|
||||||
|
User::factory()->create([
|
||||||
|
'nama' => 'Kasir',
|
||||||
|
'role' => 'kasir',
|
||||||
|
'password' => bcrypt('123123'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
User::factory(2)->create();
|
User::factory(2)->create();
|
||||||
@ -77,17 +82,24 @@ class DatabaseSeeder extends Seeder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Transaksi::factory(20)->create()->each(function ($transaksi) {
|
Transaksi::factory(40)->create()->each(function ($transaksi) {
|
||||||
$jumlah_item = rand(1, 5);
|
$jumlah_item = rand(1, 2);
|
||||||
$items = Item::where('is_sold', false)->inRandomOrder()->limit($jumlah_item)->get();
|
$items = Item::where('is_sold', false)->inRandomOrder()->limit($jumlah_item)->get();
|
||||||
if ($items->isEmpty()) return;
|
if ($items->isEmpty()) return;
|
||||||
|
$total_harga = $transaksi->total_harga;
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
$transaksi->itemTransaksi()->create([
|
$transaksi->itemTransaksi()->create([
|
||||||
'id_item' => $item->id,
|
'id_item' => $item->id,
|
||||||
'harga_deal' => $item->produk->harga_jual,
|
'harga_deal' => $item->produk->harga_jual,
|
||||||
|
'posisi_asal' => $item->id_nampan ? 'Nampan ' . $item->nampan->nama : 'Brankas',
|
||||||
]);
|
]);
|
||||||
$item->update(['is_sold' => true]);
|
$item->update([
|
||||||
|
'id_nampan' => null,
|
||||||
|
'is_sold' => true,
|
||||||
|
]);
|
||||||
|
$total_harga += $item->produk->harga_jual;
|
||||||
}
|
}
|
||||||
|
$transaksi->update(['total_harga' => $total_harga]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,11 @@ const error = ref(null);
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get("/api/item"); // ganti sesuai URL backend
|
const res = await axios.get("/api/item",{
|
||||||
|
headers:{
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
}
|
||||||
|
}); // ganti sesuai URL backend
|
||||||
items.value = res.data; // pastikan backend return array of items
|
items.value = res.data; // pastikan backend return array of items
|
||||||
console.log(res.data);
|
console.log(res.data);
|
||||||
|
|
||||||
|
@ -1,44 +1,39 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
|
<div
|
||||||
|
class="fixed inset-0 flex items-center justify-center bg-black/65 z-50"
|
||||||
|
>
|
||||||
<div class="bg-white rounded-lg p-6 w-96 shadow-lg">
|
<div class="bg-white rounded-lg p-6 w-96 shadow-lg">
|
||||||
<h2 class="text-lg font-bold mb-4">Tambah Akun</h2>
|
<h2 class="text-lg font-bold mb-4">Tambah Akun</h2>
|
||||||
|
|
||||||
<form @submit.prevent="createAkun">
|
<form @submit.prevent="createAkun" class="space-y-3">
|
||||||
<!-- Nama -->
|
<!-- Nama -->
|
||||||
<div class="mb-3">
|
<label for="nama">Nama</label>
|
||||||
<label class="block font-medium mb-1">Nama</label>
|
<InputField
|
||||||
<input
|
v-model="form.nama"
|
||||||
v-model.trim="form.nama"
|
id="nama"
|
||||||
type="text"
|
type="text"
|
||||||
class="border rounded w-full p-2 focus:ring focus:ring-blue-300"
|
:required="true"
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Password -->
|
<div>
|
||||||
<div class="mb-3">
|
<label for="password">Password</label>
|
||||||
<label class="block font-medium mb-1">Password</label>
|
<InputField
|
||||||
<input
|
|
||||||
v-model="form.password"
|
v-model="form.password"
|
||||||
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
class="border rounded w-full p-2 focus:ring focus:ring-blue-300"
|
:required="true"
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Peran -->
|
<label for="peran">Peran</label>
|
||||||
<div class="mb-3">
|
<InputSelect
|
||||||
<label class="block font-medium mb-1">Peran</label>
|
|
||||||
<select
|
|
||||||
v-model="form.role"
|
v-model="form.role"
|
||||||
class="border rounded w-full p-2 focus:ring focus:ring-blue-300"
|
:options="[
|
||||||
required
|
{ value: 'owner', label: 'Owner' },
|
||||||
>
|
{ value: 'kasir', label: 'Kasir' },
|
||||||
<option disabled value="">-- Pilih Peran --</option>
|
]"
|
||||||
<option value="owner">Owner</option>
|
placeholder="-- Pilih Peran --"
|
||||||
<option value="kasir">Kasir</option>
|
/>
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tombol -->
|
<!-- Tombol -->
|
||||||
<div class="flex justify-end gap-2 mt-4">
|
<div class="flex justify-end gap-2 mt-4">
|
||||||
@ -68,28 +63,29 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import InputField from "@/components/InputField.vue";
|
||||||
|
import InputSelect from "@/components/InputSelect.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "CreateAkun",
|
name: "CreateAkun",
|
||||||
|
components: { InputField, InputSelect },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
form: {
|
form: { nama: "", password: "", role: "" },
|
||||||
nama: "",
|
|
||||||
password: "",
|
|
||||||
role: "",
|
|
||||||
},
|
|
||||||
errorMessage: "",
|
errorMessage: "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async createAkun() {
|
async createAkun() {
|
||||||
try {
|
try {
|
||||||
await axios.post("api/user", this.form);
|
await axios.post("api/user", this.form, {
|
||||||
|
headers: {
|
||||||
// reset form
|
Authorization: `Bearer ${localStorage.getItem(
|
||||||
|
"token"
|
||||||
|
)}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
this.form = { nama: "", password: "", role: "" };
|
this.form = { nama: "", password: "", role: "" };
|
||||||
|
|
||||||
// tutup modal dan refresh data
|
|
||||||
this.$emit("refresh");
|
this.$emit("refresh");
|
||||||
this.$emit("close");
|
this.$emit("close");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -113,7 +113,11 @@ const selectedNampanName = computed(() => {
|
|||||||
// Methods
|
// Methods
|
||||||
const loadNampanList = async () => {
|
const loadNampanList = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/nampan');
|
const response = await axios.get('/api/nampan', {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});;
|
||||||
nampanList.value = response.data;
|
nampanList.value = response.data;
|
||||||
positionListOptions.value = [
|
positionListOptions.value = [
|
||||||
{ value: '', label: 'Brankas', selected: !selectedNampan.value },
|
{ value: '', label: 'Brankas', selected: !selectedNampan.value },
|
||||||
@ -142,7 +146,11 @@ const createItem = async () => {
|
|||||||
payload.id_nampan = selectedNampan.value;
|
payload.id_nampan = selectedNampan.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await axios.post('/api/item', payload);
|
const response = await axios.post('/api/item', payload, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});;
|
||||||
|
|
||||||
success.value = true;
|
success.value = true;
|
||||||
createdItem.value = response.data.data
|
createdItem.value = response.data.data
|
||||||
|
@ -11,18 +11,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Form -->
|
<!-- Form -->
|
||||||
<div class="mb-4">
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700">Nama Kategori</label>
|
<label class="block text-sm font-medium text-gray-700">Nama Kategori</label>
|
||||||
<input
|
<InputField
|
||||||
v-model="form.nama"
|
v-model="form.nama"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Masukkan nama kategori"
|
placeholder="Masukkan nama kategori"
|
||||||
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring focus:ring-C"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Buttons -->
|
<!-- Buttons -->
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2 mt-4">
|
||||||
<button
|
<button
|
||||||
@click="emit('close')"
|
@click="emit('close')"
|
||||||
class="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300"
|
class="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300"
|
||||||
@ -44,6 +43,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import InputField from './InputField.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isOpen: Boolean,
|
isOpen: Boolean,
|
||||||
@ -61,9 +61,17 @@
|
|||||||
const saveKategori = async () => {
|
const saveKategori = async () => {
|
||||||
try {
|
try {
|
||||||
if (props.product) {
|
if (props.product) {
|
||||||
await axios.put(`/api/kategori/${props.product.id}`, form.value)
|
await axios.put(`/api/kategori/${props.product.id}`, form.value, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
await axios.post('/api/kategori', form.value)
|
await axios.post('/api/kategori', form.value, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
emit('close') // tutup modal
|
emit('close') // tutup modal
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -7,24 +7,41 @@
|
|||||||
<h2 class="text-xl font-bold mb-4">Tambah Sales</h2>
|
<h2 class="text-xl font-bold mb-4">Tambah Sales</h2>
|
||||||
|
|
||||||
<form @submit.prevent="handleSubmit" class="space-y-4">
|
<form @submit.prevent="handleSubmit" class="space-y-4">
|
||||||
|
<!-- Nama -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700">Nama Sales</label>
|
<label class="block text-sm font-medium text-gray-700">Nama Sales</label>
|
||||||
<input v-model="form.nama" type="text" class="w-full px-3 py-2 border rounded-md focus:ring-2 focus:ring-[#c6a77d] focus:outline-none" required />
|
<InputField v-model="form.nama" type="text" placeholder="Masukkan nama sales" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700">No HP</label>
|
<label class="block text-sm font-medium text-gray-700">No HP</label>
|
||||||
<input v-model="form.no_hp" type="text" class="w-full px-3 py-2 border rounded-md focus:ring-2 focus:ring-[#c6a77d] focus:outline-none" required />
|
<InputField v-model="form.no_hp" type="text" placeholder="Masukkan nomor HP" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700">Alamat</label>
|
<label class="block text-sm font-medium text-gray-700">Alamat</label>
|
||||||
<textarea v-model="form.alamat" class="w-full px-3 py-2 border rounded-md focus:ring-2 focus:ring-[#c6a77d] focus:outline-none" required></textarea>
|
<textarea
|
||||||
|
v-model="form.alamat"
|
||||||
|
placeholder="Masukkan alamat"
|
||||||
|
class="mt-1 block w-full rounded-md shadow-sm sm:text-sm bg-A text-D border-B focus:border-C focus:ring focus:ring-D focus:ring-opacity-50 p-2"
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end gap-2 mt-6">
|
<div class="flex justify-end gap-2 mt-6">
|
||||||
<button type="button" @click="$emit('close')" class="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400">Batal</button>
|
<button
|
||||||
<button type="submit" class="px-4 py-2 bg-C text-D rounded hover:bg-C/80">Simpan</button>
|
type="button"
|
||||||
|
@click="$emit('close')"
|
||||||
|
class="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400"
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="px-4 py-2 bg-C text-D rounded hover:bg-C/80"
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -32,27 +49,38 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from "vue";
|
import { ref } from "vue"
|
||||||
import axios from "axios";
|
import axios from "axios"
|
||||||
|
import InputField from "./InputField.vue"
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isOpen: Boolean,
|
isOpen: Boolean,
|
||||||
});
|
})
|
||||||
|
|
||||||
const emit = defineEmits(["close"]);
|
const emit = defineEmits(["close", "saved"])
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
nama: "",
|
nama: "",
|
||||||
no_hp: "",
|
no_hp: "",
|
||||||
alamat: "",
|
alamat: "",
|
||||||
});
|
})
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
form.value = { nama: "", no_hp: "", alamat: "" }
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
await axios.post("/api/sales", form.value);
|
await axios.post("/api/sales", form.value, {
|
||||||
emit("close");
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
resetForm()
|
||||||
|
emit("saved")
|
||||||
|
emit("close")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating sales:", error);
|
console.error("Error creating sales:", error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
100
resources/js/components/DetailLaporan.vue
Normal file
100
resources/js/components/DetailLaporan.vue
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<div class="my-6">
|
||||||
|
<hr class="border-B mb-5" />
|
||||||
|
<div class="flex flex-row mb-3 overflow-x-auto">
|
||||||
|
<input type="date" v-model="tanggalDipilih"
|
||||||
|
class="mt-1 block rounded-md shadow-sm sm:text-sm bg-A text-D border-B focus:border-C focus:ring focus:outline-none focus:ring-D focus:ring-opacity-50 p-2" />
|
||||||
|
<InputSelect class="ml-3" :options="opsiSales" v-model="salesDipilih" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 overflow-x-auto">
|
||||||
|
<table class="w-full border-collapse border border-C rounded-md">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-C text-D rounded-t-md">
|
||||||
|
<th class="border-x border-C px-3 py-3">Nama Produk</th>
|
||||||
|
<th class="border-x border-C px-3 py-3">Item Terjual</th>
|
||||||
|
<th class="border-x border-C px-3 py-3">Total Berat</th>
|
||||||
|
<th class="border-x border-C px-3 py-3">Total Pendapatan</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-if="loading">
|
||||||
|
<td colspan="5" class="p-4">
|
||||||
|
<div class="flex items-center justify-center w-full h-30">
|
||||||
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-D"></div>
|
||||||
|
<span class="ml-2 text-gray-600">Memuat data...</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-else-if="!produk.length">
|
||||||
|
<td colspan="5" class="p-4 text-center">Tidak ada data untuk ditampilkan.</td>
|
||||||
|
</tr>
|
||||||
|
<template v-else v-for="item in produk" :key="item.nama_produk">
|
||||||
|
<tr class="hover:bg-B">
|
||||||
|
<td class="border-x border-C px-3 py-2 text-center">{{ item.nama_produk }}</td>
|
||||||
|
<td class="border-x border-C px-3 py-2 text-center">{{ item.jumlah_item_terjual }}</td>
|
||||||
|
<td class="border-x border-C px-3 py-2 text-center">{{ item.berat_terjual }} gr</td>
|
||||||
|
<td class="border-x border-C px-3 py-2 text-center">Rp {{ item.pendapatan }}</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, watch, computed } from 'vue';
|
||||||
|
import InputSelect from './InputSelect.vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const tanggalDipilih = ref('');
|
||||||
|
const data = ref(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const produk = computed(() => data.value?.produk || []);
|
||||||
|
|
||||||
|
const salesDipilih = ref(null);
|
||||||
|
const opsiSales = ref([
|
||||||
|
{ label: 'Semua Sales', value: null, selected: true },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const fetchSales = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/sales');
|
||||||
|
const salesData = response.data;
|
||||||
|
opsiSales.value = [{ label: 'Semua Sales', value: null }, ...salesData.map(sales => ({
|
||||||
|
label: sales.nama,
|
||||||
|
value: sales.id,
|
||||||
|
}))];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Gagal mengambil data sales:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchData = async (date) => {
|
||||||
|
if (!date) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/api/detail-laporan?tanggal=${date}`);
|
||||||
|
data.value = response.data;;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Gagal mengambil data laporan:', error);
|
||||||
|
data.value = null;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
tanggalDipilih.value = today;
|
||||||
|
|
||||||
|
fetchSales();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(tanggalDipilih, (newDate) => {
|
||||||
|
fetchData(newDate);
|
||||||
|
}, { immediate: true });
|
||||||
|
</script>
|
@ -1,38 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
|
<div class="fixed inset-0 flex items-center justify-center bg-black/65">
|
||||||
<div class="bg-white rounded-lg p-6 w-96">
|
<div class="bg-white rounded-lg p-6 w-96">
|
||||||
<h2 class="text-lg font-bold mb-4">Edit Akun</h2>
|
<h2 class="text-lg font-bold mb-4">Edit Akun</h2>
|
||||||
|
|
||||||
<form @submit.prevent="updateAkun">
|
<form @submit.prevent="updateAkun" class="space-y-3">
|
||||||
<!-- Nama -->
|
<label for="nama">Nama</label>
|
||||||
<div class="mb-3">
|
<InputField
|
||||||
<label class="block font-medium">Nama</label>
|
v-model="form.nama"
|
||||||
<input v-model="form.nama" type="text" class="border rounded w-full p-2" required />
|
label="nama"
|
||||||
|
type="text"
|
||||||
|
:required="true"
|
||||||
|
class="mb-3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<InputField
|
||||||
|
v-model="form.password"
|
||||||
|
label="password"
|
||||||
|
type="password"
|
||||||
|
:required="false"
|
||||||
|
class="mb-1"
|
||||||
|
/>
|
||||||
|
<p class="text-sm">Kosongkan jika tidak ingin ubah password</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Password -->
|
<label for="peran">Peran</label>
|
||||||
<div class="mb-3">
|
<InputSelect
|
||||||
<label class="block font-medium">Password</label>
|
v-model="form.role"
|
||||||
<input v-model="form.password" type="password" class="border rounded w-full p-2" />
|
label="peran"
|
||||||
<small class="text-gray-500">Kosongkan jika tidak ingin mengubah password</small>
|
:options="[
|
||||||
</div>
|
{ value: 'owner', label: 'Owner' },
|
||||||
|
{ value: 'kasir', label: 'Kasir' }
|
||||||
|
]"
|
||||||
|
:required="true"
|
||||||
|
class="mb-3"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Peran -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="block font-medium">Peran</label>
|
|
||||||
<select v-model="form.role" class="border rounded w-full p-2" required>
|
|
||||||
<option value="">-- Pilih Peran --</option>
|
|
||||||
<option value="owner">Owner</option>
|
|
||||||
<option value="kasir">Kasir</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tombol -->
|
|
||||||
<div class="flex justify-end gap-2 mt-4">
|
<div class="flex justify-end gap-2 mt-4">
|
||||||
<button type="button" @click="$emit('close')" class="bg-gray-300 px-4 py-2 rounded">
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="$emit('close')"
|
||||||
|
class="bg-gray-300 px-4 py-2 rounded"
|
||||||
|
>
|
||||||
Batal
|
Batal
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded">
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="bg-blue-500 text-white px-4 py-2 rounded"
|
||||||
|
>
|
||||||
Ubah
|
Ubah
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -43,6 +59,8 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import InputField from "@/components/InputField.vue";
|
||||||
|
import InputSelect from "@/components/InputSelect.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
@ -51,12 +69,14 @@
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
components: { InputField, InputSelect },
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
form: {
|
form: {
|
||||||
nama: this.akun.nama || "",
|
nama: this.akun.nama || "",
|
||||||
password: "",
|
password: "",
|
||||||
role: this.akun.role || "", // gunakan "role" bukan "peran"
|
role: this.akun.role || "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -81,13 +101,16 @@
|
|||||||
const payload = { ...this.form };
|
const payload = { ...this.form };
|
||||||
if (!payload.password) delete payload.password;
|
if (!payload.password) delete payload.password;
|
||||||
|
|
||||||
await axios.put(`api/user/${this.akun.id}`, payload);
|
await axios.put(`/api/user/${this.akun.id}`, payload, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});;
|
||||||
|
|
||||||
this.$emit("refresh");
|
this.$emit("refresh");
|
||||||
this.$emit("close");
|
this.$emit("close");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Gagal update akun:", err.response?.data || err.message);
|
console.error("Gagal update akun:", err.response?.data || err.message);
|
||||||
alert("Update akun gagal. Silakan cek kembali inputan.");
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -11,14 +11,13 @@
|
|||||||
<h2 class="text-xl font-bold text-center text-D mb-4">Edit Kategori</h2>
|
<h2 class="text-xl font-bold text-center text-D mb-4">Edit Kategori</h2>
|
||||||
|
|
||||||
<!-- Input Nama Kategori -->
|
<!-- Input Nama Kategori -->
|
||||||
<div class="mb-4">
|
<div>
|
||||||
<label for="editKategori" class="block text-sm font-medium text-D mb-1">Nama Kategori</label>
|
<label for="editKategori" class="block text-sm font-medium text-D mb-1">Nama Kategori</label>
|
||||||
<input
|
<InputField
|
||||||
v-model="editNamaKategori"
|
v-model="editNamaKategori"
|
||||||
type="text"
|
type="text"
|
||||||
id="editKategori"
|
id="editKategori"
|
||||||
placeholder="Masukkan nama kategori"
|
placeholder="Masukkan nama kategori"
|
||||||
class="w-full p-2 border rounded-md bg-Focus outline-none"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -37,6 +36,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from "vue";
|
import { ref, watch } from "vue";
|
||||||
|
import InputField from "./InputField.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
kategori: { type: Object, required: true },
|
kategori: { type: Object, required: true },
|
||||||
|
@ -9,17 +9,21 @@
|
|||||||
<form @submit.prevent="handleSubmit" class="space-y-4">
|
<form @submit.prevent="handleSubmit" class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700">Nama Sales</label>
|
<label class="block text-sm font-medium text-gray-700">Nama Sales</label>
|
||||||
<input v-model="form.nama" type="text" class="w-full px-3 py-2 border rounded-md focus:ring-2 focus:ring-[#c6a77d] focus:outline-none" required />
|
<InputField v-model="form.nama" type="text" class="w-full px-3 py-2 border rounded-md focus:ring-2 focus:ring-[#c6a77d] focus:outline-none" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700">No HP</label>
|
<label class="block text-sm font-medium text-gray-700">No HP</label>
|
||||||
<input v-model="form.no_hp" type="text" class="w-full px-3 py-2 border rounded-md focus:ring-2 focus:ring-[#c6a77d] focus:outline-none" required />
|
<InputField v-model="form.no_hp" type="text" class="w-full px-3 py-2 border rounded-md focus:ring-2 focus:ring-[#c6a77d] focus:outline-none" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700">Alamat</label>
|
<label class="block text-sm font-medium text-gray-700">Alamat</label>
|
||||||
<textarea v-model="form.alamat" class="w-full px-3 py-2 border rounded-md focus:ring-2 focus:ring-[#c6a77d] focus:outline-none" required></textarea>
|
<textarea
|
||||||
|
v-model="form.alamat"
|
||||||
|
class="mt-1 block w-full rounded-md shadow-sm sm:text-sm bg-A text-D border-B focus:border-C focus:ring focus:ring-D focus:ring-opacity-50 p-2"
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end gap-2 mt-6">
|
<div class="flex justify-end gap-2 mt-6">
|
||||||
@ -34,6 +38,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from "vue";
|
import { ref, watch } from "vue";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import InputField from "./InputField.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isOpen: Boolean,
|
isOpen: Boolean,
|
||||||
@ -60,7 +65,11 @@
|
|||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
await axios.put(`/api/sales/${props.sales.id}`, form.value);
|
await axios.put(`/api/sales/${props.sales.id}`, form.value, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});;
|
||||||
emit("close");
|
emit("close");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating sales:", error);
|
console.error("Error updating sales:", error);
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
@input="$emit('update:modelValue', $event.target.value)"
|
@input="$emit('update:modelValue', $event.target.value)"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
class="mt-1 block w-full rounded-md shadow-sm sm:text-sm bg-A text-D border-B focus:border-C focus:ring focus:ring-D focus:ring-opacity-50 p-2"
|
class="mt-1 block w-full rounded-md shadow-sm sm:text-sm bg-A text-D border-B focus:border-C focus:ring focus:outline-none focus:ring-D focus:ring-opacity-50 p-2"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
46
resources/js/components/InputPassword.vue
Normal file
46
resources/js/components/InputPassword.vue
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative mb-8">
|
||||||
|
<input
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
:value="modelValue"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
@input="$emit('update:modelValue', $event.target.value)"
|
||||||
|
class="mt-1 block w-full rounded-md shadow-sm sm:text-sm
|
||||||
|
bg-A text-D border-B focus:border-C
|
||||||
|
focus:ring focus:ring-D focus:ring-opacity-50 p-2 pr-10"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Tombol show/hide password -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="togglePassword"
|
||||||
|
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-500 hover:text-gray-700 focus:outline-none"
|
||||||
|
>
|
||||||
|
<i v-if="showPassword" class="fas fa-eye"></i>
|
||||||
|
<i v-else class="fas fa-eye-slash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: "Password",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
|
|
||||||
|
const showPassword = ref(false);
|
||||||
|
|
||||||
|
const togglePassword = () => {
|
||||||
|
showPassword.value = !showPassword.value;
|
||||||
|
};
|
||||||
|
</script>
|
@ -6,7 +6,7 @@
|
|||||||
<label class="block text-sm font-medium text-D">Kode Item *</label>
|
<label class="block text-sm font-medium text-D">Kode Item *</label>
|
||||||
<div class="flex flex-row justify-between mt-1 w-full rounded-md bg-A shadow-sm sm:text-sm border-B">
|
<div class="flex flex-row justify-between mt-1 w-full rounded-md bg-A shadow-sm sm:text-sm border-B">
|
||||||
<input type="text" v-model="kodeItem" @keyup.enter="inputItem" placeholder="Scan atau masukkan kode item"
|
<input type="text" v-model="kodeItem" @keyup.enter="inputItem" placeholder="Scan atau masukkan kode item"
|
||||||
class=" bg-A focus:border-C focus:ring focus:ring-D focus:ring-opacity-50 p-2 w-full" />
|
class=" bg-A focus:outline-none focus:border-C focus:ring focus:ring-D focus:ring-opacity-50 p-2 w-full rounded-l-md" />
|
||||||
<button v-if="!loadingItem" @click="inputItem" class="px-3 bg-D hover:bg-D/80 text-A rounded-r-md"><i
|
<button v-if="!loadingItem" @click="inputItem" class="px-3 bg-D hover:bg-D/80 text-A rounded-r-md"><i
|
||||||
class="fas fa-arrow-right"></i></button>
|
class="fas fa-arrow-right"></i></button>
|
||||||
<div v-else class="flex items-center justify-center px-3">
|
<div v-else class="flex items-center justify-center px-3">
|
||||||
@ -97,7 +97,11 @@ const inputItem = async () => {
|
|||||||
loadingItem.value = true
|
loadingItem.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`/api/item/${kodeItem.value}`);
|
const response = await axios.get(`/api/item/${kodeItem.value}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});;
|
||||||
item.value = response.data;
|
item.value = response.data;
|
||||||
hargaJual.value = item.value.produk.harga_jual
|
hargaJual.value = item.value.produk.harga_jual
|
||||||
|
|
||||||
|
@ -1,26 +1,42 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, provide } from "vue";
|
import { ref, provide, computed } from "vue";
|
||||||
import NavDesktop from "./NavDesktop.vue";
|
import NavDesktop from "./NavDesktop.vue";
|
||||||
import NavMobile from "./NavMobile.vue";
|
import NavMobile from "./NavMobile.vue";
|
||||||
import logo from "../../images/logo.png";
|
import logo from "../../images/logo.png";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
const isOpen = ref(false);
|
const isOpen = ref(false);
|
||||||
const isMobileMenuOpen = ref(false);
|
const isMobileMenuOpen = ref(false);
|
||||||
const openDropdownIndex = ref(null);
|
const openDropdownIndex = ref(null);
|
||||||
|
|
||||||
const items = [
|
const baseItems = [
|
||||||
{ label: "Manajemen Produk", subItems: [
|
{
|
||||||
|
label: "Manajemen Produk",
|
||||||
|
subItems: [
|
||||||
{ label: "Brankas", route: "/brankas" },
|
{ label: "Brankas", route: "/brankas" },
|
||||||
{ label: "Nampan", route: "/nampan" },
|
{ label: "Nampan", route: "/nampan" },
|
||||||
{ label: "Produk", route: "/produk" },
|
{ label: "Produk", route: "/produk" },
|
||||||
{ label: "Kategori", route: "/kategori" },
|
{ label: "Kategori", route: "/kategori" },
|
||||||
{ label: "Sales", route: "/sales" },
|
{ label: "Sales", route: "/sales" },
|
||||||
] },
|
]
|
||||||
|
},
|
||||||
{ label: "Kasir", route: "/kasir" },
|
{ label: "Kasir", route: "/kasir" },
|
||||||
{ label: "Laporan", route: "/laporan" },
|
{ label: "Laporan", route: "/laporan" },
|
||||||
{ label: "Akun", route: "/akun" },
|
{ label: "Akun", route: "/akun" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const role = localStorage.getItem("role");
|
||||||
|
|
||||||
|
const items = computed(() => {
|
||||||
|
if (role === "owner") {
|
||||||
|
return baseItems;
|
||||||
|
}
|
||||||
|
if (role === "kasir") {
|
||||||
|
return baseItems.filter(item => !["Akun", "Laporan"].includes(item.label));
|
||||||
|
}
|
||||||
|
return baseItems;
|
||||||
|
});
|
||||||
|
|
||||||
const toggleDropdown = (index = null) => {
|
const toggleDropdown = (index = null) => {
|
||||||
if (index !== null) {
|
if (index !== null) {
|
||||||
openDropdownIndex.value = openDropdownIndex.value === index ? null : index;
|
openDropdownIndex.value = openDropdownIndex.value === index ? null : index;
|
||||||
@ -41,13 +57,31 @@ const closeMobileMenu = () => {
|
|||||||
openDropdownIndex.value = null;
|
openDropdownIndex.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = () => {
|
const logout = async () => {
|
||||||
console.log("Logout clicked");
|
try {
|
||||||
|
await axios.post("/api/logout", null, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
localStorage.removeItem("token");
|
||||||
|
localStorage.removeItem("role");
|
||||||
|
|
||||||
|
window.location.href = "/";
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response && error.response.status === 401) {
|
||||||
|
localStorage.removeItem("token");
|
||||||
|
localStorage.removeItem("role");
|
||||||
|
window.location.href = "/";
|
||||||
|
} else {
|
||||||
|
console.error("Logout failed:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
closeMobileMenu();
|
closeMobileMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Provide shared data to child components
|
// Provide shared data to child components
|
||||||
provide('navigationData', {
|
provide("navigationData", {
|
||||||
logo,
|
logo,
|
||||||
items,
|
items,
|
||||||
isOpen,
|
isOpen,
|
||||||
@ -69,10 +103,7 @@ provide('navigationData', {
|
|||||||
<NavMobile />
|
<NavMobile />
|
||||||
|
|
||||||
<!-- Click Outside Handler for Desktop Dropdown -->
|
<!-- Click Outside Handler for Desktop Dropdown -->
|
||||||
<div
|
<div v-if="openDropdownIndex !== null && !isMobileMenuOpen" @click="openDropdownIndex = null"
|
||||||
v-if="openDropdownIndex !== null && !isMobileMenuOpen"
|
class="fixed inset-0 z-10"></div>
|
||||||
@click="openDropdownIndex = null"
|
|
||||||
class="fixed inset-0 z-10"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
245
resources/js/components/RingkasanLaporanA.vue
Normal file
245
resources/js/components/RingkasanLaporanA.vue
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-row items-center justify-end mt-5 gap-3">
|
||||||
|
<div class="relative w-32" ref="filterDropdownRef">
|
||||||
|
<button @click="isFilterOpen = !isFilterOpen" type="button"
|
||||||
|
class="flex items-center justify-between w-full px-3 py-2 text-sm text-left bg-C border rounded-md border-C hover:bg-C/80 focus:outline-none">
|
||||||
|
<span>{{ selectedFilterLabel }}</span>
|
||||||
|
<i class="fas fa-chevron-down"></i>
|
||||||
|
</button>
|
||||||
|
<div v-if="isFilterOpen" class="absolute z-10 w-full mt-1 bg-C border rounded-md shadow-lg border-C">
|
||||||
|
<ul class="py-1">
|
||||||
|
<li v-for="option in filterOptions" :key="option.value" @click="selectFilter(option)"
|
||||||
|
class="px-3 py-2 text-sm cursor-pointer text-D hover:bg-B">
|
||||||
|
{{ option.label }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative w-40" ref="exportDropdownRef">
|
||||||
|
<button @click="isExportOpen = !isExportOpen" type="button"
|
||||||
|
class="flex items-center justify-between w-full px-3 py-2 text-sm text-left bg-C border rounded-md border-C hover:bg-C/80 focus:outline-none">
|
||||||
|
<span :class="{ 'text-D': !exportFormat }">{{ selectedExportLabel }}</span>
|
||||||
|
<i class="fas fa-chevron-down"></i>
|
||||||
|
</button>
|
||||||
|
<div v-if="isExportOpen" class="absolute z-10 w-full mt-1 bg-C border rounded-md shadow-lg border-C">
|
||||||
|
<ul class="py-1">
|
||||||
|
<li v-for="option in exportOptions" :key="option.value" @click="selectExport(option)"
|
||||||
|
class="px-3 py-2 text-sm cursor-pointer text-D hover:bg-B">
|
||||||
|
{{ option.label }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5 overflow-x-auto">
|
||||||
|
<table class="w-full border-collapse border border-C rounded-md">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-C text-D rounded-t-md">
|
||||||
|
<th class="border-x border-C px-3 py-3">Tanggal</th>
|
||||||
|
<th class="border-x border-C px-3 py-3">Nama Sales</th>
|
||||||
|
<th class="border-x border-C px-3 py-3">Jumlah Item Terjual</th>
|
||||||
|
<th class="border-x border-C px-3 py-3">Total Berat Terjual</th>
|
||||||
|
<th class="border-x border-C px-3 py-3">Total Pendapatan</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-if="loading">
|
||||||
|
<td colspan="5" class="p-4">
|
||||||
|
<div class="flex items-center justify-center w-full h-30">
|
||||||
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-D"></div>
|
||||||
|
<span class="ml-2 text-gray-600">Memuat data...</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-else-if="!ringkasanLaporan.length">
|
||||||
|
<td colspan="5" class="p-4 text-center">Tidak ada data untuk ditampilkan.</td>
|
||||||
|
</tr>
|
||||||
|
<template v-else v-for="item in ringkasanLaporan" :key="item.tanggal">
|
||||||
|
<template v-if="item.sales && item.sales.length > 0">
|
||||||
|
<tr class="text-center border-y border-C"
|
||||||
|
:class="item.sales[0].item_terjual == 0 ? 'bg-red-200 hover:bg-red-300' : 'hover:bg-A'">
|
||||||
|
<td class="px-3 py-2 border-x border-C bg-white" :rowspan="item.sales.length">{{
|
||||||
|
item.tanggal }}</td>
|
||||||
|
<td class="px-3 py-2 border-x border-C text-left">{{ item.sales[0].nama }}</td>
|
||||||
|
<td class="px-3 py-2 border-x border-C">{{ item.sales[0].item_terjual }}</td>
|
||||||
|
<td class="px-3 py-2 border-x border-C">{{ item.sales[0].berat_terjual }}</td>
|
||||||
|
<td class="flex justify-center">
|
||||||
|
<div :ref="el => { if (el) pendapatanElements.push(el) }" :style="pendapatanStyle" :class="item.sales[0].pendapatan == '-' ? 'text-center' : 'text-right'">
|
||||||
|
{{ item.sales[0].pendapatan }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="sales in item.sales.slice(1)" :key="sales.nama"
|
||||||
|
class="text-center border-y border-C"
|
||||||
|
:class="sales.item_terjual == '-' ? 'bg-red-200 hover:bg-red-300' : 'hover:bg-A'">
|
||||||
|
<td class="px-3 py-2 text-left border-x border-C">{{ sales.nama }}</td>
|
||||||
|
<td class="px-3 py-2 border-x border-C">{{ sales.item_terjual }}</td>
|
||||||
|
<td class="px-3 py-2 border-x border-C">{{ sales.berat_terjual }}</td>
|
||||||
|
<td class="flex justify-center">
|
||||||
|
<div :ref="el => { if (el) pendapatanElements.push(el) }" :style="pendapatanStyle" :class="sales.pendapatan == '-' ? 'text-center' : 'text-right'">
|
||||||
|
{{ sales.pendapatan }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="font-semibold text-center border-y border-C bg-B hover:bg-C/80">
|
||||||
|
<td class="px-3 py-2 border-x border-C" colspan="2">Total</td>
|
||||||
|
<td class="px-3 py-2 border-x border-C">{{ item.total_item_terjual }}</td>
|
||||||
|
<td class="px-3 py-2 border-x border-C">{{ item.total_berat }}</td>
|
||||||
|
<td class="flex justify-center">
|
||||||
|
<div :ref="el => { if (el) pendapatanElements.push(el) }" :style="pendapatanStyle" :class="item.sales[0].pendapatan == '-' ? 'text-center' : 'text-right'">
|
||||||
|
{{ item.total_pendapatan }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<tr class="text-center border-y border-C hover:bg-A">
|
||||||
|
<td class="px-3 py-2 border-x border-C">{{ item.tanggal }}</td>
|
||||||
|
<td colspan="4" class="px-3 py-2 italic text-gray-500 border-x border-C bg-yellow-50 hover:bg-yellow-100">Tidak ada transaksi
|
||||||
|
pada hari ini</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div v-if="pagination.total > 0 && pagination.last_page > 1" class="flex items-center justify-end gap-2 mt-4">
|
||||||
|
<button @click="goToPage(pagination.current_page - 1)" :disabled="pagination.current_page === 1 || loading"
|
||||||
|
class="px-2 py-1 text-sm font-medium border rounded-md bg-C border-C disabled:opacity-50 disabled:cursor-not-allowed hover:bg-C/80">
|
||||||
|
Sebelumnya
|
||||||
|
</button>
|
||||||
|
<span class="text-sm text-D">
|
||||||
|
Halaman {{ pagination.current_page }} dari {{ pagination.last_page }}
|
||||||
|
</span>
|
||||||
|
<button @click="goToPage(pagination.current_page + 1)"
|
||||||
|
:disabled="(pagination.current_page === pagination.last_page) || loading"
|
||||||
|
class="px-2 py-1 text-sm font-medium border rounded-md bg-C border-C disabled:opacity-50 disabled:cursor-not-allowed hover:bg-C/80">
|
||||||
|
Berikutnya
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted, watch, computed, nextTick } from "vue";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
// --- State ---
|
||||||
|
const isFilterOpen = ref(false);
|
||||||
|
const isExportOpen = ref(false);
|
||||||
|
const filterDropdownRef = ref(null);
|
||||||
|
const exportDropdownRef = ref(null);
|
||||||
|
|
||||||
|
const filterOptions = ref([
|
||||||
|
{ value: 'bulan', label: 'Bulanan' },
|
||||||
|
{ value: 'hari', label: 'Harian' }
|
||||||
|
]);
|
||||||
|
const exportOptions = ref([
|
||||||
|
{ value: 'pdf', label: 'Pdf' },
|
||||||
|
{ value: 'xls', label: 'Excel' },
|
||||||
|
{ value: 'csv', label: 'Csv' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const filterRingkasan = ref("bulan");
|
||||||
|
const exportFormat = ref(null);
|
||||||
|
const ringkasanLaporan = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const pagination = ref({
|
||||||
|
current_page: 1,
|
||||||
|
last_page: 1,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pendapatanWidth = ref(0);
|
||||||
|
const pendapatanElements = ref([]);
|
||||||
|
|
||||||
|
// --- Computed ---
|
||||||
|
const selectedFilterLabel = computed(() => {
|
||||||
|
return filterOptions.value.find(opt => opt.value === filterRingkasan.value)?.label;
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedExportLabel = computed(() => {
|
||||||
|
return exportOptions.value.find(opt => opt.value === exportFormat.value)?.label || 'Export Laporan';
|
||||||
|
});
|
||||||
|
|
||||||
|
const pendapatanStyle = computed(() => ({
|
||||||
|
minWidth: `${pendapatanWidth.value}px`,
|
||||||
|
padding: '0.5rem 0.75rem'
|
||||||
|
}));
|
||||||
|
|
||||||
|
// --- Watchers ---
|
||||||
|
watch(ringkasanLaporan, async (newValue) => {
|
||||||
|
if (newValue && newValue.length > 0) {
|
||||||
|
await nextTick();
|
||||||
|
let maxWidth = 0;
|
||||||
|
pendapatanElements.value.forEach(el => {
|
||||||
|
if (el && el.scrollWidth > maxWidth) {
|
||||||
|
maxWidth = el.scrollWidth;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pendapatanWidth.value = maxWidth;
|
||||||
|
}
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
// --- Methods ---
|
||||||
|
const fetchRingkasan = async (page = 1) => {
|
||||||
|
loading.value = true;
|
||||||
|
pendapatanElements.value = [];
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/api/laporan?filter=${filterRingkasan.value}&page=${page}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});;
|
||||||
|
ringkasanLaporan.value = response.data.data;
|
||||||
|
pagination.value = {
|
||||||
|
current_page: response.data.current_page,
|
||||||
|
last_page: response.data.last_page,
|
||||||
|
total: response.data.total,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching laporan:", error);
|
||||||
|
ringkasanLaporan.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToPage = (page) => {
|
||||||
|
if (page >= 1 && page <= pagination.value.last_page) {
|
||||||
|
fetchRingkasan(page);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectFilter = (option) => {
|
||||||
|
filterRingkasan.value = option.value;
|
||||||
|
isFilterOpen.value = false;
|
||||||
|
goToPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectExport = (option) => {
|
||||||
|
exportFormat.value = option.value;
|
||||||
|
isExportOpen.value = false;
|
||||||
|
alert(`Fitur Belum dikerjakan. Laporan akan diekspor dalam format ${option.label}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDropdownsOnClickOutside = (event) => {
|
||||||
|
if (filterDropdownRef.value && !filterDropdownRef.value.contains(event.target)) {
|
||||||
|
isFilterOpen.value = false;
|
||||||
|
}
|
||||||
|
if (exportDropdownRef.value && !exportDropdownRef.value.contains(event.target)) {
|
||||||
|
isExportOpen.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Lifecycle Hooks ---
|
||||||
|
onMounted(() => {
|
||||||
|
fetchRingkasan(pagination.value.current_page);
|
||||||
|
document.addEventListener('click', closeDropdownsOnClickOutside);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', closeDropdownsOnClickOutside);
|
||||||
|
});
|
||||||
|
</script>
|
243
resources/js/components/RingkasanLaporanB.vue
Normal file
243
resources/js/components/RingkasanLaporanB.vue
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-row items-center justify-end mt-5 gap-3">
|
||||||
|
<div class="relative w-32" ref="filterDropdownRef">
|
||||||
|
<button @click="isFilterOpen = !isFilterOpen" type="button"
|
||||||
|
class="flex items-center justify-between w-full px-3 py-2 text-sm text-left bg-C border rounded-md border-C hover:bg-C/80 focus:outline-none">
|
||||||
|
<span>{{ selectedFilterLabel }}</span>
|
||||||
|
<i class="fas fa-chevron-down"></i>
|
||||||
|
</button>
|
||||||
|
<div v-if="isFilterOpen" class="absolute z-10 w-full mt-1 bg-C border rounded-md shadow-lg border-C">
|
||||||
|
<ul class="py-1">
|
||||||
|
<li v-for="option in filterOptions" :key="option.value" @click="selectFilter(option)"
|
||||||
|
class="px-3 py-2 text-sm cursor-pointer text-D hover:bg-B">
|
||||||
|
{{ option.label }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative w-40" ref="exportDropdownRef">
|
||||||
|
<button @click="isExportOpen = !isExportOpen" type="button"
|
||||||
|
class="flex items-center justify-between w-full px-3 py-2 text-sm text-left bg-C border rounded-md border-C hover:bg-C/80 focus:outline-none">
|
||||||
|
<span :class="{ 'text-D': !exportFormat }">{{ selectedExportLabel }}</span>
|
||||||
|
<i class="fas fa-chevron-down"></i>
|
||||||
|
</button>
|
||||||
|
<div v-if="isExportOpen" class="absolute z-10 w-full mt-1 bg-C border rounded-md shadow-lg border-C">
|
||||||
|
<ul class="py-1">
|
||||||
|
<li v-for="option in exportOptions" :key="option.value" @click="selectExport(option)"
|
||||||
|
class="px-3 py-2 text-sm cursor-pointer text-D hover:bg-B">
|
||||||
|
{{ option.label }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5 overflow-x-auto">
|
||||||
|
<table class="w-full border-collapse border border-C rounded-md">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-C text-D rounded-t-md">
|
||||||
|
<th class="border-x border-C px-3 py-3">Tanggal</th>
|
||||||
|
<th class="border-x border-C px-3 py-3">Nama Sales</th>
|
||||||
|
<th class="border-x border-C px-3 py-3">Jumlah Item Terjual</th>
|
||||||
|
<th class="border-x border-C px-3 py-3">Total Berat Terjual</th>
|
||||||
|
<th class="border-x border-C px-3 py-3">Total Pendapatan</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-if="loading">
|
||||||
|
<td colspan="5" class="p-4">
|
||||||
|
<div class="flex items-center justify-center w-full h-30">
|
||||||
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-D"></div>
|
||||||
|
<span class="ml-2 text-gray-600">Memuat data...</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-else-if="!ringkasanLaporan.length">
|
||||||
|
<td colspan="5" class="p-4 text-center">Tidak ada data untuk ditampilkan.</td>
|
||||||
|
</tr>
|
||||||
|
<template v-else v-for="item in ringkasanLaporan" :key="item.tanggal">
|
||||||
|
<template v-if="item.sales && item.sales.length > 0">
|
||||||
|
<tr class="text-center border-y border-C hover:bg-A">
|
||||||
|
<td class="px-3 py-2 border-x border-C bg-white" :rowspan="item.sales.length">{{
|
||||||
|
item.tanggal }}</td>
|
||||||
|
<td class="px-3 py-2 border-x border-C text-left">{{ item.sales[0].nama }}</td>
|
||||||
|
<td class="px-3 py-2 border-x border-C">{{ item.sales[0].item_terjual }}</td>
|
||||||
|
<td class="px-3 py-2 border-x border-C">{{ item.sales[0].berat_terjual }}</td>
|
||||||
|
<td class="flex justify-center">
|
||||||
|
<div :ref="el => { if (el) pendapatanElements.push(el) }" :style="pendapatanStyle" :class="item.sales[0].pendapatan == '-' ? 'text-center' : 'text-right'">
|
||||||
|
{{ item.sales[0].pendapatan }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="sales in item.sales.slice(1)" :key="sales.nama"
|
||||||
|
class="text-center border-y border-C hover:bg-A">
|
||||||
|
<td class="px-3 py-2 text-left border-x border-C">{{ sales.nama }}</td>
|
||||||
|
<td class="px-3 py-2 border-x border-C">{{ sales.item_terjual }}</td>
|
||||||
|
<td class="px-3 py-2 border-x border-C">{{ sales.berat_terjual }}</td>
|
||||||
|
<td class="flex justify-center">
|
||||||
|
<div :ref="el => { if (el) pendapatanElements.push(el) }" :style="pendapatanStyle" :class="sales.pendapatan == '-' ? 'text-center' : 'text-right'">
|
||||||
|
{{ sales.pendapatan }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="font-semibold text-center border-y border-C bg-B hover:bg-C/80">
|
||||||
|
<td class="px-3 py-2 border-x border-C" colspan="2">Total</td>
|
||||||
|
<td class="px-3 py-2 border-x border-C">{{ item.total_item_terjual }}</td>
|
||||||
|
<td class="px-3 py-2 border-x border-C">{{ item.total_berat }}</td>
|
||||||
|
<td class="flex justify-center">
|
||||||
|
<div :ref="el => { if (el) pendapatanElements.push(el) }" :style="pendapatanStyle" :class="item.sales[0].pendapatan == '-' ? 'text-center' : 'text-right'">
|
||||||
|
{{ item.total_pendapatan }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<tr class="text-center border-y border-C hover:bg-A">
|
||||||
|
<td class="px-3 py-2 border-x border-C">{{ item.tanggal }}</td>
|
||||||
|
<td colspan="4" class="px-3 py-2 italic text-gray-500 border-x border-C">Tidak ada transaksi
|
||||||
|
pada hari ini</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div v-if="pagination.total > 0 && pagination.last_page > 1" class="flex items-center justify-end gap-2 mt-4">
|
||||||
|
<button @click="goToPage(pagination.current_page - 1)" :disabled="pagination.current_page === 1 || loading"
|
||||||
|
class="px-2 py-1 text-sm font-medium border rounded-md bg-C border-C disabled:opacity-50 disabled:cursor-not-allowed hover:bg-C/80">
|
||||||
|
Sebelumnya
|
||||||
|
</button>
|
||||||
|
<span class="text-sm text-D">
|
||||||
|
Halaman {{ pagination.current_page }} dari {{ pagination.last_page }}
|
||||||
|
</span>
|
||||||
|
<button @click="goToPage(pagination.current_page + 1)"
|
||||||
|
:disabled="(pagination.current_page === pagination.last_page) || loading"
|
||||||
|
class="px-2 py-1 text-sm font-medium border rounded-md bg-C border-C disabled:opacity-50 disabled:cursor-not-allowed hover:bg-C/80">
|
||||||
|
Berikutnya
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted, watch, computed, nextTick } from "vue";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
// --- State ---
|
||||||
|
const isFilterOpen = ref(false);
|
||||||
|
const isExportOpen = ref(false);
|
||||||
|
const filterDropdownRef = ref(null);
|
||||||
|
const exportDropdownRef = ref(null);
|
||||||
|
|
||||||
|
const filterOptions = ref([
|
||||||
|
{ value: 'bulan', label: 'Bulanan' },
|
||||||
|
{ value: 'hari', label: 'Harian' }
|
||||||
|
]);
|
||||||
|
const exportOptions = ref([
|
||||||
|
{ value: 'pdf', label: 'Pdf' },
|
||||||
|
{ value: 'xls', label: 'Excel' },
|
||||||
|
{ value: 'csv', label: 'Csv' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const filterRingkasan = ref("bulan");
|
||||||
|
const exportFormat = ref(null);
|
||||||
|
const ringkasanLaporan = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const pagination = ref({
|
||||||
|
current_page: 1,
|
||||||
|
last_page: 1,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pendapatanWidth = ref(0);
|
||||||
|
const pendapatanElements = ref([]);
|
||||||
|
|
||||||
|
// --- Computed ---
|
||||||
|
const selectedFilterLabel = computed(() => {
|
||||||
|
return filterOptions.value.find(opt => opt.value === filterRingkasan.value)?.label;
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedExportLabel = computed(() => {
|
||||||
|
return exportOptions.value.find(opt => opt.value === exportFormat.value)?.label || 'Export Laporan';
|
||||||
|
});
|
||||||
|
|
||||||
|
const pendapatanStyle = computed(() => ({
|
||||||
|
minWidth: `${pendapatanWidth.value}px`,
|
||||||
|
padding: '0.5rem 0.75rem'
|
||||||
|
}));
|
||||||
|
|
||||||
|
// --- Watchers ---
|
||||||
|
watch(ringkasanLaporan, async (newValue) => {
|
||||||
|
if (newValue && newValue.length > 0) {
|
||||||
|
await nextTick();
|
||||||
|
let maxWidth = 0;
|
||||||
|
pendapatanElements.value.forEach(el => {
|
||||||
|
if (el && el.scrollWidth > maxWidth) {
|
||||||
|
maxWidth = el.scrollWidth;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pendapatanWidth.value = maxWidth;
|
||||||
|
}
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
// --- Methods ---
|
||||||
|
const fetchRingkasan = async (page = 1) => {
|
||||||
|
loading.value = true;
|
||||||
|
pendapatanElements.value = [];
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/api/laporan?filter=${filterRingkasan.value}&page=${page}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});;
|
||||||
|
ringkasanLaporan.value = response.data.data;
|
||||||
|
pagination.value = {
|
||||||
|
current_page: response.data.current_page,
|
||||||
|
last_page: response.data.last_page,
|
||||||
|
total: response.data.total,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching laporan:", error);
|
||||||
|
ringkasanLaporan.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToPage = (page) => {
|
||||||
|
if (page >= 1 && page <= pagination.value.last_page) {
|
||||||
|
fetchRingkasan(page);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectFilter = (option) => {
|
||||||
|
filterRingkasan.value = option.value;
|
||||||
|
isFilterOpen.value = false;
|
||||||
|
goToPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectExport = (option) => {
|
||||||
|
exportFormat.value = option.value;
|
||||||
|
isExportOpen.value = false;
|
||||||
|
alert(`Fitur Belum dikerjakan. Laporan akan diekspor dalam format ${option.label}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDropdownsOnClickOutside = (event) => {
|
||||||
|
if (filterDropdownRef.value && !filterDropdownRef.value.contains(event.target)) {
|
||||||
|
isFilterOpen.value = false;
|
||||||
|
}
|
||||||
|
if (exportDropdownRef.value && !exportDropdownRef.value.contains(event.target)) {
|
||||||
|
isExportOpen.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Lifecycle Hooks ---
|
||||||
|
onMounted(() => {
|
||||||
|
fetchRingkasan(pagination.value.current_page);
|
||||||
|
document.addEventListener('click', closeDropdownsOnClickOutside);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', closeDropdownsOnClickOutside);
|
||||||
|
});
|
||||||
|
</script>
|
@ -138,10 +138,18 @@ const closePopup = () => {
|
|||||||
const saveMove = async () => {
|
const saveMove = async () => {
|
||||||
if (!selectedTrayId.value || !selectedItem.value) return;
|
if (!selectedTrayId.value || !selectedItem.value) return;
|
||||||
try {
|
try {
|
||||||
await axios.put(`/api/item/${selectedItem.value.id}`, {
|
await axios.put(`/api/item/${selectedItem.value.id}`,
|
||||||
|
{
|
||||||
id_nampan: selectedTrayId.value,
|
id_nampan: selectedTrayId.value,
|
||||||
id_produk: selectedItem.value.id_produk, // ikutkan id_produk karena API minta
|
id_produk: selectedItem.value.id_produk,
|
||||||
});
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
await refreshData();
|
await refreshData();
|
||||||
closePopup();
|
closePopup();
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
v-model="searchText"
|
v-model="searchText"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Cari ..."
|
placeholder="Cari ..."
|
||||||
class="border rounded-md px-3 py-2 w-64 focus:outline-none focus:ring-2 focus:ring-blue-400"
|
class="border border-C bg-A rounded-md px-3 py-2 w-64 focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||||
@input="$emit('update:search', searchText)"
|
@input="$emit('update:search', searchText)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
9
resources/js/middlewares/auth.js
Normal file
9
resources/js/middlewares/auth.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export default function auth(to, from, next) {
|
||||||
|
const token = localStorage.getItem("token")
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
next({ name: "Login" })
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
8
resources/js/middlewares/guest.js
Normal file
8
resources/js/middlewares/guest.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export default function guest(to, from, next) {
|
||||||
|
const token = localStorage.getItem("token")
|
||||||
|
if (token) {
|
||||||
|
next({ name: "Brankas" })
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
8
resources/js/middlewares/owner.js
Normal file
8
resources/js/middlewares/owner.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export default function owner(to, from, next) {
|
||||||
|
const role = localStorage.getItem("role")
|
||||||
|
if (role !== "owner") {
|
||||||
|
next({ name: "Kasir" })
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
@ -51,14 +51,14 @@
|
|||||||
|
|
||||||
<!-- Table Section -->
|
<!-- Table Section -->
|
||||||
<div
|
<div
|
||||||
class="bg-white rounded-lg shadow-md border border-gray-200 overflow-hidden"
|
class="bg-white rounded-lg shadow-md border border-C overflow-hidden"
|
||||||
>
|
>
|
||||||
<table class="w-full">
|
<table class="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-C text-white">
|
<tr class="bg-C text-white">
|
||||||
<th class="px-6 py-4 text-center text-D border-r border-[#b09065]">No</th>
|
<th class="px-6 py-4 text-center text-D border-r border-C">No</th>
|
||||||
<th class="px-6 py-4 text-center text-D border-r border-[#b09065]">Nama</th>
|
<th class="px-6 py-4 text-center text-D border-r border-C">Nama</th>
|
||||||
<th class="px-6 py-4 text-center text-D border-r border-[#b09065]">Role</th>
|
<th class="px-6 py-4 text-center text-D border-r border-C">Peran</th>
|
||||||
<th class="px-6 py-4 text-center text-D">Aksi</th>
|
<th class="px-6 py-4 text-center text-D">Aksi</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -66,16 +66,16 @@
|
|||||||
<tr
|
<tr
|
||||||
v-for="(item, index) in akun"
|
v-for="(item, index) in akun"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="border-b border-gray-200 hover:bg-gray-50 transition duration-150"
|
class="border-b border-C hover:bg-gray-50 transition duration-150"
|
||||||
:class="{ 'bg-gray-50': index % 2 === 1 }"
|
:class="{ 'bg-gray-50': index % 2 === 1 }"
|
||||||
>
|
>
|
||||||
<td class="px-6 py-4 border-r border-gray-200 text-center font-medium text-gray-900">
|
<td class="px-6 py-4 border-r border-C text-center font-medium text-gray-900">
|
||||||
{{ index + 1 }}
|
{{ index + 1 }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 border-r border-gray-200 text-D">
|
<td class="px-6 py-4 border-r border-C text-D">
|
||||||
{{ item.nama }}
|
{{ item.nama }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 border-r border-gray-200 text-gray-800">
|
<td class="px-6 py-4 border-r border-C text-gray-800">
|
||||||
{{ item.role }}
|
{{ item.role }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-center">
|
<td class="px-6 py-4 text-center">
|
||||||
@ -151,7 +151,11 @@ const akunToDelete = ref(null);
|
|||||||
const fetchAkun = async () => {
|
const fetchAkun = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await axios.get("/api/user");
|
const response = await axios.get("/api/user", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
akun.value = response.data;
|
akun.value = response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching akun:", error);
|
console.error("Error fetching akun:", error);
|
||||||
@ -180,7 +184,11 @@ const hapusAkun = (item) => {
|
|||||||
|
|
||||||
const confirmDelete = async () => {
|
const confirmDelete = async () => {
|
||||||
try {
|
try {
|
||||||
await axios.delete(`/api/user/${akunToDelete.value.id}`);
|
await axios.delete(`/api/user/${akunToDelete.value.id}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
fetchAkun();
|
fetchAkun();
|
||||||
confirmDeleteOpen.value = false;
|
confirmDeleteOpen.value = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -15,33 +15,64 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="block text-D mb-1">Nama Produk</label>
|
<label class="block text-D mb-1">Nama Produk</label>
|
||||||
<InputField v-model="form.nama" type="text" placeholder="Masukkan nama produk" />
|
<InputField
|
||||||
|
v-model="form.nama"
|
||||||
|
type="text"
|
||||||
|
placeholder="Masukkan nama produk"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="block text-D mb-1">Kategori</label>
|
<label class="block text-D mb-1">Kategori</label>
|
||||||
<InputSelect v-model="form.id_kategori" :options="category" placeholder="Pilih kategori" />
|
<InputSelect
|
||||||
|
v-model="form.id_kategori"
|
||||||
|
:options="category"
|
||||||
|
placeholder="Pilih kategori"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3 flex flex-row w-full gap-3">
|
<div class="mb-3 flex flex-row w-full gap-3">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<label class="block text-D mb-1">Berat (g)</label>
|
<label class="block text-D mb-1">Berat (g)</label>
|
||||||
<InputField v-model="form.berat" type="number" step="0.01" placeholder="Masukkan berat" @input="calculateHargaJual" />
|
<InputField
|
||||||
|
v-model="form.berat"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
placeholder="Masukkan berat"
|
||||||
|
@input="calculateHargaJual"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<label class="block text-D mb-1">Kadar (K)</label>
|
<label class="block text-D mb-1">Kadar (K)</label>
|
||||||
<InputField v-model="form.kadar" type="number" placeholder="Masukkan kadar" />
|
<InputField
|
||||||
|
v-model="form.kadar"
|
||||||
|
type="number"
|
||||||
|
placeholder="Masukkan kadar"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3 flex flex-row w-full gap-3">
|
<div class="mb-3 flex flex-row w-full gap-3">
|
||||||
<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"
|
||||||
<InputField v-model="form.harga_per_gram" type="number" step="0.01" placeholder="Masukkan harga per gram" @input="calculateHargaJual" />
|
>Harga per Gram</label
|
||||||
|
>
|
||||||
|
<InputField
|
||||||
|
v-model="form.harga_per_gram"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
placeholder="Masukkan harga per gram"
|
||||||
|
@input="calculateHargaJual"
|
||||||
|
/>
|
||||||
</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 v-model="form.harga_jual" type="number" step="0.01" placeholder="Masukkan harga jual" />
|
<InputField
|
||||||
|
v-model="form.harga_jual"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
placeholder="Masukkan harga jual"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -52,57 +83,133 @@
|
|||||||
|
|
||||||
<div class="grid grid-cols-3 gap-3">
|
<div class="grid grid-cols-3 gap-3">
|
||||||
<!-- Existing Images -->
|
<!-- Existing Images -->
|
||||||
<div v-for="(image, index) in uploadedImages" :key="`img-${image.id}`" class="relative group aspect-square">
|
<div
|
||||||
<div class="w-full h-full bg-gray-100 rounded-lg border-2 border-gray-200 overflow-hidden">
|
v-for="(image, index) in uploadedImages"
|
||||||
<img :src="image.url" :alt="`Foto ${index + 1}`" class="w-full h-full object-cover" />
|
:key="`img-${image.id}`"
|
||||||
<button @click="removeImage(image.id)" :disabled="uploadLoading"
|
class="relative group aspect-square"
|
||||||
class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm font-bold shadow-lg hover:bg-red-600 transition-colors disabled:bg-gray-400">
|
>
|
||||||
|
<div
|
||||||
|
class="w-full h-full bg-gray-100 rounded-lg border-2 border-gray-200 overflow-hidden"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="image.url"
|
||||||
|
:alt="`Foto ${index + 1}`"
|
||||||
|
class="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="removeImage(image.id)"
|
||||||
|
:disabled="uploadLoading"
|
||||||
|
class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm font-bold shadow-lg hover:bg-red-600 transition-colors disabled:bg-gray-400"
|
||||||
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Upload Button -->
|
<!-- Upload Button -->
|
||||||
<div v-if="uploadedImages.length < 6" @drop="handleDrop" @dragover.prevent
|
<div
|
||||||
@dragenter.prevent="isDragging = true" @dragleave.prevent="isDragging = false" @click="triggerFileInput"
|
v-if="uploadedImages.length < 6"
|
||||||
|
@drop="handleDrop"
|
||||||
|
@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="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 }">
|
:class="{
|
||||||
|
'border-blue-400 bg-blue-50': isDragging,
|
||||||
|
'cursor-not-allowed opacity-50': uploadLoading,
|
||||||
|
}"
|
||||||
|
>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div v-if="!uploadLoading"
|
<div
|
||||||
class="w-12 h-12 bg-D rounded-lg flex items-center justify-center mx-auto mb-2 group-hover:bg-D transition-colors">
|
v-if="!uploadLoading"
|
||||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
class="w-12 h-12 bg-D rounded-lg flex items-center justify-center mx-auto mb-2 group-hover:bg-D transition-colors"
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
>
|
||||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-12 h-12 bg-D rounded-lg flex items-center justify-center mx-auto mb-2">
|
<div
|
||||||
<svg class="animate-spin w-6 h-6 text-white" fill="none" viewBox="0 0 24 24">
|
v-else
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
class="w-12 h-12 bg-D rounded-lg flex items-center justify-center mx-auto mb-2"
|
||||||
<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">
|
<svg
|
||||||
</path>
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-600 font-medium" v-html="uploadLoading ? 'Uploading...' : 'Unggah<br/>Foto'"></p>
|
<p
|
||||||
|
class="text-xs text-gray-600 font-medium"
|
||||||
|
v-html="
|
||||||
|
uploadLoading
|
||||||
|
? 'Uploading...'
|
||||||
|
: 'Unggah<br/>Foto'
|
||||||
|
"
|
||||||
|
></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input ref="fileInput" type="file" multiple accept="image/jpeg,image/jpg,image/png" @change="handleFileSelect" class="hidden" />
|
<input
|
||||||
|
ref="fileInput"
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept="image/jpeg,image/jpg,image/png"
|
||||||
|
@change="handleFileSelect"
|
||||||
|
class="hidden"
|
||||||
|
/>
|
||||||
|
|
||||||
<p class="text-xs text-gray-500 mt-2">Format: JPG, JPEG, PNG (Max: 2MB per file, Max: 6 foto)</p>
|
<p class="text-xs text-gray-500 mt-2">
|
||||||
|
Format: JPG, JPEG, PNG (Max: 2MB per file, Max: 6 foto)
|
||||||
|
</p>
|
||||||
|
|
||||||
<div v-if="uploadError" class="mt-2 p-2 bg-red-50 border border-red-200 rounded text-sm text-red-600">
|
<div
|
||||||
|
v-if="uploadError"
|
||||||
|
class="mt-2 p-2 bg-red-50 border border-red-200 rounded text-sm text-red-600"
|
||||||
|
>
|
||||||
{{ uploadError }}
|
{{ uploadError }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex justify-end flex-row gap-3">
|
<div class="mt-6 flex justify-end flex-row gap-3">
|
||||||
<button @click="back" class="px-6 py-2 rounded-md bg-gray-400 hover:bg-gray-500 text-white">Batal</button>
|
<button
|
||||||
<button @click="submitForm" :disabled="loading || !isFormValid"
|
@click="back"
|
||||||
class="bg-C text-D px-6 py-2 rounded-md hover:bg-B disabled:bg-B disabled:text-white disabled:cursor-not-allowed">
|
class="px-6 py-2 rounded-md bg-gray-400 hover:bg-gray-500 text-white"
|
||||||
{{ loading ? 'Menyimpan...' : 'Simpan Perubahan' }}
|
>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="submitForm"
|
||||||
|
:disabled="loading || !isFormValid"
|
||||||
|
class="bg-C text-D px-6 py-2 rounded-md hover:bg-B disabled:bg-B disabled:text-white disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{{ loading ? "Menyimpan..." : "Simpan Perubahan" }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -164,12 +271,20 @@ const calculateHargaJual = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loadKategori = async () => {
|
const loadKategori = async () => {
|
||||||
const response = await axios.get("/api/kategori");
|
const response = await axios.get("/api/kategori", {
|
||||||
|
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 }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadProduk = async () => {
|
const loadProduk = async () => {
|
||||||
const response = await axios.get(`/api/produk/${productId}`);
|
const response = await axios.get(`/api/produk/${productId}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
const produk = response.data;
|
const produk = response.data;
|
||||||
form.value = {
|
form.value = {
|
||||||
nama: produk.nama,
|
nama: produk.nama,
|
||||||
@ -216,7 +331,10 @@ const uploadFiles = async (files) => {
|
|||||||
formData.append("foto", file);
|
formData.append("foto", file);
|
||||||
formData.append("id_user", userId.value);
|
formData.append("id_user", userId.value);
|
||||||
const res = await axios.post("/api/foto/upload", formData, {
|
const res = await axios.post("/api/foto/upload", formData, {
|
||||||
headers: { "Content-Type": "multipart/form-data" },
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
uploadedImages.value.push(res.data);
|
uploadedImages.value.push(res.data);
|
||||||
}
|
}
|
||||||
@ -227,7 +345,11 @@ const uploadFiles = async (files) => {
|
|||||||
|
|
||||||
const removeImage = async (id) => {
|
const removeImage = async (id) => {
|
||||||
try {
|
try {
|
||||||
await axios.delete(`/api/foto/hapus/${id}`);
|
await axios.delete(`/api/foto/hapus/${id}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
uploadedImages.value = uploadedImages.value.filter((i) => i.id !== id);
|
uploadedImages.value = uploadedImages.value.filter((i) => i.id !== id);
|
||||||
} catch {
|
} catch {
|
||||||
uploadError.value = "Gagal menghapus foto";
|
uploadError.value = "Gagal menghapus foto";
|
||||||
@ -237,10 +359,18 @@ const removeImage = async (id) => {
|
|||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
await axios.put(`/api/produk/${productId}`, {
|
await axios.put(
|
||||||
|
`/api/produk/${productId}`,
|
||||||
|
{
|
||||||
...form.value,
|
...form.value,
|
||||||
id_user: userId.value,
|
id_user: userId.value,
|
||||||
});
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
alert("Produk berhasil diupdate!");
|
alert("Produk berhasil diupdate!");
|
||||||
router.push("/produk");
|
router.push("/produk");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -6,7 +6,6 @@
|
|||||||
:product="createdProduct"
|
:product="createdProduct"
|
||||||
@close="closeItemModal"
|
@close="closeItemModal"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<p class="font-serif italic text-[25px] text-D">Produk Baru</p>
|
<p class="font-serif italic text-[25px] text-D">Produk Baru</p>
|
||||||
|
|
||||||
@ -131,6 +130,7 @@ import InputField from "../components/InputField.vue";
|
|||||||
import InputSelect from "../components/InputSelect.vue";
|
import InputSelect from "../components/InputSelect.vue";
|
||||||
import CreateItemModal from "../components/CreateItemModal.vue";
|
import CreateItemModal from "../components/CreateItemModal.vue";
|
||||||
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
@ -146,7 +146,11 @@ const category = ref([]);
|
|||||||
|
|
||||||
const loadKategori = async () => {
|
const loadKategori = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/kategori');
|
const response = await axios.get('/api/kategori', {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
if (response.data && Array.isArray(response.data)) {
|
if (response.data && Array.isArray(response.data)) {
|
||||||
category.value = response.data.map(cat => ({
|
category.value = response.data.map(cat => ({
|
||||||
value: cat.id,
|
value: cat.id,
|
||||||
@ -190,7 +194,11 @@ const calculateHargaJual = () => {
|
|||||||
|
|
||||||
const loadExistingPhotos = async () => {
|
const loadExistingPhotos = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`/api/foto/${userId.value}`);
|
const response = await axios.get(`/api/foto/${userId.value}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
if (response.data && Array.isArray(response.data)) {
|
if (response.data && Array.isArray(response.data)) {
|
||||||
uploadedImages.value = response.data;
|
uploadedImages.value = response.data;
|
||||||
}
|
}
|
||||||
@ -271,7 +279,9 @@ const uploadFiles = async (files) => {
|
|||||||
|
|
||||||
const response = await axios.post('/api/foto/upload', formData, {
|
const response = await axios.post('/api/foto/upload', formData, {
|
||||||
headers: {
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -292,7 +302,12 @@ const uploadFiles = async (files) => {
|
|||||||
|
|
||||||
const removeImage = async (imageId) => {
|
const removeImage = async (imageId) => {
|
||||||
try {
|
try {
|
||||||
await axios.delete(`/api/foto/hapus/${imageId}`);
|
await axios.delete(`/api/foto/hapus/${imageId}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
;
|
||||||
uploadedImages.value = uploadedImages.value.filter(img => img.id !== imageId);
|
uploadedImages.value = uploadedImages.value.filter(img => img.id !== imageId);
|
||||||
uploadError.value = '';
|
uploadError.value = '';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -312,7 +327,10 @@ const submitForm = async (addItem) => {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.post('/api/produk', {
|
const response = await axios.post('/api/produk', {
|
||||||
...form.value,
|
...form.value,
|
||||||
id_user: userId.value
|
id_user: userId.value,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const createdProductData = response.data.data;
|
const createdProductData = response.data.data;
|
||||||
@ -363,7 +381,11 @@ const resetForm = async () => {
|
|||||||
harga_jual: 0,
|
harga_jual: 0,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await axios.delete(`/api/foto/reset/${userId.value}`);
|
await axios.delete(`/api/foto/reset/${userId.value}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
uploadedImages.value = [];
|
uploadedImages.value = [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error resetting photos:', error);
|
console.error('Error resetting photos:', error);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<mainLayout>
|
<mainLayout>
|
||||||
<div class="lg:p-2 pt-6">
|
<div class="lg:p-2 pt-6">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-5 gap-3 sm:gap-2 max-w-7xl mx-auto">
|
<div class="grid grid-cols-1 lg:grid-cols-5 gap-3 sm:gap-2 mx-auto min-h-[75vh]">
|
||||||
<!-- Left Section - Form Kasir -->
|
<!-- Left Section - Form Kasir -->
|
||||||
<div class="lg:col-span-3">
|
<div class="lg:col-span-3">
|
||||||
<div class="bg-white rounded-xl shadow-lg border border-B overflow-hidden h-full">
|
<div class="bg-white rounded-xl shadow-lg border border-B overflow-hidden h-full">
|
||||||
@ -59,7 +59,11 @@ const loading = ref(true)
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const res = await axios.get("/api/transaksi?limit=10")
|
const res = await axios.get("/api/transaksi?limit=10", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
transaksi.value = res.data
|
transaksi.value = res.data
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Table Section -->
|
<!-- Table Section -->
|
||||||
<div class="bg-white rounded-lg shadow-md border border-gray-200 overflow-hidden">
|
<div class="bg-white rounded-lg shadow-md border border-C overflow-hidden">
|
||||||
<table class="w-full">
|
<table class="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-C text-black">
|
<tr class="bg-C text-black">
|
||||||
@ -35,12 +35,12 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(item, index) in kategori" :key="item.id"
|
<tr v-for="(item, index) in kategori" :key="item.id"
|
||||||
class="border-b border-gray-200 hover:bg-A transition duration-150"
|
class="border-b border-C hover:bg-A transition duration-150"
|
||||||
:class="{ 'bg-gray-50': index % 2 === 1 }">
|
:class="{ 'bg-gray-50': index % 2 === 1 }">
|
||||||
<td class="px-6 py-4 border-r border-gray-200 font-medium text-center text-gray-900">
|
<td class="px-6 py-4 border-r border-C font-medium text-center text-gray-900">
|
||||||
{{ index + 1 }}
|
{{ index + 1 }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 border-r border-gray-200 text-center text-gray-800">
|
<td class="px-6 py-4 border-r border-C text-center text-gray-800">
|
||||||
{{ item.nama }}
|
{{ item.nama }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-center">
|
<td class="px-6 py-4 text-center">
|
||||||
@ -101,7 +101,11 @@ const kategoriToDelete = ref(null);
|
|||||||
const fetchKategoris = async () => {
|
const fetchKategoris = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await axios.get("/api/kategori");
|
const response = await axios.get("/api/kategori", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
kategori.value = response.data;
|
kategori.value = response.data;
|
||||||
console.log("Data kategori:", response.data);
|
console.log("Data kategori:", response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -138,7 +142,11 @@ const hapusKategori = (item) => {
|
|||||||
// 🔵 Ditambahkan: aksi konfirmasi hapus
|
// 🔵 Ditambahkan: aksi konfirmasi hapus
|
||||||
const confirmDelete = async () => {
|
const confirmDelete = async () => {
|
||||||
try {
|
try {
|
||||||
await axios.delete(`/api/kategori/${kategoriToDelete.value.id}`);
|
await axios.delete(`/api/kategori/${kategoriToDelete.value.id}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
console.log("Kategori berhasil dihapus");
|
console.log("Kategori berhasil dihapus");
|
||||||
fetchKategoris();
|
fetchKategoris();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
18
resources/js/pages/Laporan.vue
Normal file
18
resources/js/pages/Laporan.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<mainLayout>
|
||||||
|
<div class="p-6">
|
||||||
|
<p class="font-serif italic text-[25px] text-D">Laporan</p>
|
||||||
|
|
||||||
|
<RingkasanLaporanB />
|
||||||
|
|
||||||
|
<DetailLaporan />
|
||||||
|
</div>
|
||||||
|
</mainLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import DetailLaporan from '../components/DetailLaporan.vue';
|
||||||
|
import RingkasanLaporanA from '../components/RingkasanLaporanA.vue';
|
||||||
|
import RingkasanLaporanB from '../components/RingkasanLaporanB.vue';
|
||||||
|
import mainLayout from "../layouts/mainLayout.vue";
|
||||||
|
</script>
|
72
resources/js/pages/Login.vue
Normal file
72
resources/js/pages/Login.vue
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-center min-h-screen bg-[#0c4b66]">
|
||||||
|
<div class="bg-white p-8 rounded-2xl shadow-xl w-80 text-center">
|
||||||
|
<!-- Logo + Title -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<img :src="logo" alt="Logo" class="mx-auto w-34 py-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input -->
|
||||||
|
<div>
|
||||||
|
<InputField
|
||||||
|
v-model="username"
|
||||||
|
type="text"
|
||||||
|
placeholder="Username"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
<PasswordInput v-model="password" placeholder="Password" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Button -->
|
||||||
|
<button
|
||||||
|
@click="handleLogin"
|
||||||
|
:disabled="loading"
|
||||||
|
class="w-full py-2 bg-sky-400 hover:bg-sky-500 rounded font-bold text-gray-800 transition disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{{ loading ? "Loading..." : "Login" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
import logo from '@/../images/logo.png'
|
||||||
|
import InputField from "@/components/InputField.vue";
|
||||||
|
import PasswordInput from "@/components/InputPassword.vue";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const username = ref("");
|
||||||
|
const password = ref("");
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
if (!username.value || !password.value) {
|
||||||
|
alert("Harap isi username dan password!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await axios.post("/api/login", {
|
||||||
|
nama: username.value,
|
||||||
|
password: password.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = res.data;
|
||||||
|
|
||||||
|
// Simpan token & role
|
||||||
|
localStorage.setItem("token", data.token);
|
||||||
|
localStorage.setItem("role", data.role);
|
||||||
|
|
||||||
|
// Redirect sesuai role
|
||||||
|
window.location.href = data.redirect;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
alert("Login gagal. Periksa username atau password.");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -21,9 +21,15 @@
|
|||||||
<p class="font-serif italic text-[25px] text-D">PRODUK</p>
|
<p class="font-serif italic text-[25px] text-D">PRODUK</p>
|
||||||
|
|
||||||
<!-- Filter -->
|
<!-- Filter -->
|
||||||
<div class="mt-3 flex flex-col md:flex-row md:items-center md:justify-between gap-3">
|
<div
|
||||||
|
class="mt-3 flex flex-col md:flex-row md:items-center md:justify-between gap-3"
|
||||||
|
>
|
||||||
<!-- Dropdown Kategori -->
|
<!-- Dropdown Kategori -->
|
||||||
<InputSelect v-model="selectedCategory" :options="kategori" class="w-full md:w-48" />
|
<InputSelect
|
||||||
|
v-model="selectedCategory"
|
||||||
|
:options="kategori"
|
||||||
|
class="w-full md:w-48"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Search -->
|
<!-- Search -->
|
||||||
<searchbar v-model:search="searchQuery" class="flex-1" />
|
<searchbar v-model:search="searchQuery" class="flex-1" />
|
||||||
@ -40,7 +46,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Grid Produk -->
|
<!-- Grid Produk -->
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 mt-4">
|
<div
|
||||||
|
class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 mt-4"
|
||||||
|
>
|
||||||
<ProductCard
|
<ProductCard
|
||||||
v-for="item in filteredProducts"
|
v-for="item in filteredProducts"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
@ -102,7 +110,9 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Detail Harga & Info -->
|
<!-- Detail Harga & Info -->
|
||||||
<div class="grid grid-cols-2 gap-y-2 gap-x-4 text-sm w-full mb-6">
|
<div
|
||||||
|
class="grid grid-cols-2 gap-y-2 gap-x-4 text-sm w-full mb-6"
|
||||||
|
>
|
||||||
<p class="col-span-1">Harga Jual :</p>
|
<p class="col-span-1">Harga Jual :</p>
|
||||||
<p class="col-span-1 text-right">
|
<p class="col-span-1 text-right">
|
||||||
Rp. {{ formatNumber(detail.harga_jual) }}
|
Rp. {{ formatNumber(detail.harga_jual) }}
|
||||||
@ -171,29 +181,37 @@ const kategori = ref([]);
|
|||||||
|
|
||||||
const loadKategori = async () => {
|
const loadKategori = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/kategori');
|
const response = await axios.get("/api/kategori", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
if (response.data && Array.isArray(response.data)) {
|
if (response.data && Array.isArray(response.data)) {
|
||||||
kategori.value = [
|
kategori.value = [
|
||||||
{ value: 0, label: "Semua" },
|
{ value: 0, label: "Semua" },
|
||||||
...response.data.map(cat => ({
|
...response.data.map((cat) => ({
|
||||||
value: cat.id,
|
value: cat.id,
|
||||||
label: cat.nama
|
label: cat.nama,
|
||||||
}))
|
})),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading categories:', error);
|
console.error("Error loading categories:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadProduk = async () => {
|
const loadProduk = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/produk');
|
await axios.delete(`/api/produk/${detail.value.id}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
if (response.data && Array.isArray(response.data)) {
|
if (response.data && Array.isArray(response.data)) {
|
||||||
products.value = response.data;
|
products.value = response.data;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading products:', error);
|
console.error("Error loading products:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -207,7 +225,7 @@ const closeItemModal = () => {
|
|||||||
|
|
||||||
// Fetch data awal
|
// Fetch data awal
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
loadKategori()
|
loadKategori();
|
||||||
loadProduk();
|
loadProduk();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -216,9 +234,7 @@ const filteredProducts = computed(() => {
|
|||||||
let hasil = products.value;
|
let hasil = products.value;
|
||||||
|
|
||||||
if (selectedCategory.value != 0) {
|
if (selectedCategory.value != 0) {
|
||||||
hasil = hasil.filter(
|
hasil = hasil.filter((p) => p.id_kategori == selectedCategory.value);
|
||||||
(p) => p.id_kategori == selectedCategory.value
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchQuery.value) {
|
if (searchQuery.value) {
|
||||||
|
@ -51,7 +51,7 @@
|
|||||||
|
|
||||||
<!-- Table Section -->
|
<!-- Table Section -->
|
||||||
<div
|
<div
|
||||||
class="bg-white rounded-lg shadow-md border border-gray-200 overflow-hidden"
|
class="bg-white rounded-lg shadow-md border border-gray-200\ overflow-hidden"
|
||||||
>
|
>
|
||||||
<table class="w-full">
|
<table class="w-full">
|
||||||
<thead class="">
|
<thead class="">
|
||||||
@ -76,35 +76,33 @@
|
|||||||
>
|
>
|
||||||
Alamat
|
Alamat
|
||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-4 text-center text-D">
|
<th class="px-6 py-4 text-center text-D">Aksi</th>
|
||||||
Aksi
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<tr
|
||||||
v-for="(item, index) in sales"
|
v-for="(item, index) in sales"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="border-b border-gray-200 hover:bg-gray-50 transition duration-150"
|
class="border-b border-gray-200\ hover:bg-gray-50 transition duration-150"
|
||||||
:class="{ 'bg-gray-50': index % 2 === 1 }"
|
:class="{ 'bg-gray-50': index % 2 === 1 }"
|
||||||
>
|
>
|
||||||
<td
|
<td
|
||||||
class="px-6 py-4 border-r border-gray-200 text-center font-medium text-gray-900"
|
class="px-6 py-4 border-r border-gray-200\ text-center font-medium text-gray-900"
|
||||||
>
|
>
|
||||||
{{ index + 1 }}
|
{{ index + 1 }}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
class="px-6 py-4 border-r border-gray-200 text-D"
|
class="px-6 py-4 border-r border-gray-200\ text-D"
|
||||||
>
|
>
|
||||||
{{ item.nama }}
|
{{ item.nama }}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
class="px-6 py-4 border-r border-gray-200 text-gray-800"
|
class="px-6 py-4 border-r border-gray-200\ text-gray-800"
|
||||||
>
|
>
|
||||||
{{ item.no_hp }}
|
{{ item.no_hp }}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
class="px-6 py-4 border-r border-gray-200 text-gray-800"
|
class="px-6 py-4 border-r border-gray-200\ text-gray-800"
|
||||||
>
|
>
|
||||||
{{ item.alamat }}
|
{{ item.alamat }}
|
||||||
</td>
|
</td>
|
||||||
@ -186,7 +184,11 @@ const salesToDelete = ref(null);
|
|||||||
const fetchSales = async () => {
|
const fetchSales = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await axios.get("/api/sales");
|
const response = await axios.get("/api/sales", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
sales.value = response.data;
|
sales.value = response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching sales:", error);
|
console.error("Error fetching sales:", error);
|
||||||
@ -215,7 +217,11 @@ const hapusSales = (item) => {
|
|||||||
|
|
||||||
const confirmDelete = async () => {
|
const confirmDelete = async () => {
|
||||||
try {
|
try {
|
||||||
await axios.delete(`/api/sales/${salesToDelete.value.id}`);
|
await axios.delete(`/api/sales/${salesToDelete.value.id}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
fetchSales();
|
fetchSales();
|
||||||
confirmDeleteOpen.value = false;
|
confirmDeleteOpen.value = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<mainLayout>
|
<mainLayout>
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<!-- Judul -->
|
<!-- Judul -->
|
||||||
<p style="font-family: 'IM FELL Great Primer', serif; font-style: italic; font-size: 25px;">
|
<p
|
||||||
|
style="
|
||||||
|
font-family: 'IM FELL Great Primer', serif;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 25px;
|
||||||
|
"
|
||||||
|
>
|
||||||
NAMPAN
|
NAMPAN
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -20,21 +25,21 @@
|
|||||||
<!-- Tambah Nampan -->
|
<!-- Tambah Nampan -->
|
||||||
<button
|
<button
|
||||||
@click="openModal"
|
@click="openModal"
|
||||||
class="px-4 py-2 hover:bg-B bg-C rounded-md shadow font-semibold" >
|
class="px-4 py-2 hover:bg-B bg-C rounded-md shadow font-semibold"
|
||||||
|
>
|
||||||
Tambah Nampan
|
Tambah Nampan
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Kosongkan -->
|
<!-- Kosongkan -->
|
||||||
<button
|
<button
|
||||||
@click="openConfirmModal"
|
@click="openConfirmModal"
|
||||||
class="px-3 py-2 bg-red-500 hover:bg-red-600 text-white rounded-md">
|
class="px-3 py-2 bg-red-500 hover:bg-red-600 text-white rounded-md"
|
||||||
|
>
|
||||||
Kosongkan
|
Kosongkan
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Search + List -->
|
<!-- Search + List -->
|
||||||
|
|
||||||
<TrayList :search="searchQuery" @edit="editTray" @delete="deleteTray" />
|
<TrayList :search="searchQuery" @edit="editTray" @delete="deleteTray" />
|
||||||
@ -45,9 +50,15 @@
|
|||||||
class="fixed inset-0 bg-black/75 flex justify-center items-center z-50"
|
class="fixed inset-0 bg-black/75 flex justify-center items-center z-50"
|
||||||
>
|
>
|
||||||
<div class="bg-white rounded-lg shadow-lg p-6 w-96">
|
<div class="bg-white rounded-lg shadow-lg p-6 w-96">
|
||||||
<h2 class="text-lg font-semibold mb-4" style="color: #102C57;">Tambah Nampan</h2>
|
<h2 class="text-lg font-semibold mb-4" style="color: #102c57">
|
||||||
|
Tambah Nampan
|
||||||
|
</h2>
|
||||||
|
|
||||||
<label class="block mb-2 text-sm font-medium" style="color: #102C57;">Nama Nampan</label>
|
<label
|
||||||
|
class="block mb-2 text-sm font-medium"
|
||||||
|
style="color: #102c57"
|
||||||
|
>Nama Nampan</label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
v-model="trayName"
|
v-model="trayName"
|
||||||
type="text"
|
type="text"
|
||||||
@ -58,14 +69,16 @@
|
|||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
@click="closeModal"
|
@click="closeModal"
|
||||||
class="px-4 py-2 bg-gray-400 hover:bg-gray-500 text-white rounded-md">
|
class="px-4 py-2 bg-gray-400 hover:bg-gray-500 text-white rounded-md"
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="saveTray"
|
@click="saveTray"
|
||||||
class="px-4 py-2 bg-[#DAC0A3] hover:bg-[#C9A77E] rounded-md"
|
class="px-4 py-2 bg-[#DAC0A3] hover:bg-[#C9A77E] rounded-md"
|
||||||
style="color: #102C57;">
|
style="color: #102c57"
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -78,7 +91,9 @@
|
|||||||
class="fixed inset-0 bg-black bg-opacity-40 flex justify-center items-center z-50"
|
class="fixed inset-0 bg-black bg-opacity-40 flex justify-center items-center z-50"
|
||||||
>
|
>
|
||||||
<div class="bg-white rounded-lg shadow-lg p-6 w-96 text-center">
|
<div class="bg-white rounded-lg shadow-lg p-6 w-96 text-center">
|
||||||
<h2 class="text-xl font-bold mb-3" style="color: #102C57;">Kosongkan semua nampan?</h2>
|
<h2 class="text-xl font-bold mb-3" style="color: #102c57">
|
||||||
|
Kosongkan semua nampan?
|
||||||
|
</h2>
|
||||||
<p class="text-gray-600 mb-6">
|
<p class="text-gray-600 mb-6">
|
||||||
Semua item akan dimasukkan ke brankas. <br />
|
Semua item akan dimasukkan ke brankas. <br />
|
||||||
Masuk ke menu ‘Brankas’ untuk mengembalikan item ke nampan.
|
Masuk ke menu ‘Brankas’ untuk mengembalikan item ke nampan.
|
||||||
@ -86,95 +101,130 @@
|
|||||||
<div class="flex justify-center gap-4">
|
<div class="flex justify-center gap-4">
|
||||||
<button
|
<button
|
||||||
@click="closeConfirmModal"
|
@click="closeConfirmModal"
|
||||||
class="px-5 py-2 bg-gray-300 hover:bg-gray-400 rounded-md font-semibold">
|
class="px-5 py-2 bg-gray-300 hover:bg-gray-400 rounded-md font-semibold"
|
||||||
|
>
|
||||||
Batal
|
Batal
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="confirmEmptyTray"
|
@click="confirmEmptyTray"
|
||||||
class="px-5 py-2 bg-red-500 hover:bg-red-600 text-white rounded-md font-semibold">
|
class="px-5 py-2 bg-red-500 hover:bg-red-600 text-white rounded-md font-semibold"
|
||||||
|
>
|
||||||
Ya
|
Ya
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</mainLayout>
|
</mainLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from "vue";
|
||||||
import axios from 'axios'
|
import axios from "axios";
|
||||||
import mainLayout from '../layouts/mainLayout.vue'
|
import mainLayout from "../layouts/mainLayout.vue";
|
||||||
import searchbar from '../components/searchbar.vue'
|
import searchbar from "../components/searchbar.vue";
|
||||||
import TrayList from '../components/TrayList.vue'
|
import TrayList from "../components/TrayList.vue";
|
||||||
|
|
||||||
const searchQuery = ref("")
|
const searchQuery = ref("");
|
||||||
const showModal = ref(false)
|
const showModal = ref(false);
|
||||||
const showConfirmModal = ref(false)
|
const showConfirmModal = ref(false);
|
||||||
const trayName = ref("")
|
const trayName = ref("");
|
||||||
const editingTrayId = ref(null)
|
const editingTrayId = ref(null);
|
||||||
|
|
||||||
// buka modal tambah/edit
|
// buka modal tambah/edit
|
||||||
const openModal = () => { showModal.value = true }
|
const openModal = () => {
|
||||||
|
showModal.value = true;
|
||||||
|
};
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
trayName.value = ""
|
trayName.value = "";
|
||||||
editingTrayId.value = null
|
editingTrayId.value = null;
|
||||||
showModal.value = false
|
showModal.value = false;
|
||||||
}
|
};
|
||||||
|
|
||||||
// simpan nampan
|
// simpan nampan
|
||||||
const saveTray = async () => {
|
const saveTray = async () => {
|
||||||
if (!trayName.value.trim()) {
|
if (!trayName.value.trim()) {
|
||||||
alert("Nama Nampan tidak boleh kosong")
|
alert("Nama Nampan tidak boleh kosong");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (editingTrayId.value) {
|
if (editingTrayId.value) {
|
||||||
await axios.put(`/api/nampan/${editingTrayId.value}`, { nama: trayName.value })
|
await axios.put(
|
||||||
alert("Nampan berhasil diupdate")
|
`/api/nampan/${editingTrayId.value}`,
|
||||||
|
{ nama: trayName.value },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem(
|
||||||
|
"token"
|
||||||
|
)}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
alert("Nampan berhasil diupdate");
|
||||||
} else {
|
} else {
|
||||||
await axios.post("/api/nampan", { nama: trayName.value })
|
await axios.post(
|
||||||
alert("Nampan berhasil ditambahkan")
|
"/api/nampan",
|
||||||
|
{ nama: trayName.value },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem(
|
||||||
|
"token"
|
||||||
|
)}`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
closeModal()
|
);
|
||||||
location.reload()
|
alert("Nampan berhasil ditambahkan");
|
||||||
|
}
|
||||||
|
closeModal();
|
||||||
|
location.reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
alert("Gagal menyimpan nampan")
|
alert("Gagal menyimpan nampan");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// === Konfirmasi kosongkan nampan ===
|
// === Konfirmasi kosongkan nampan ===
|
||||||
const openConfirmModal = () => { showConfirmModal.value = true }
|
const openConfirmModal = () => {
|
||||||
const closeConfirmModal = () => { showConfirmModal.value = false }
|
showConfirmModal.value = true;
|
||||||
|
};
|
||||||
|
const closeConfirmModal = () => {
|
||||||
|
showConfirmModal.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
const confirmEmptyTray = async () => {
|
const confirmEmptyTray = async () => {
|
||||||
try {
|
try {
|
||||||
await axios.delete("/api/kosongkan-nampan",)
|
await axios.delete("/api/kosongkan-nampan", {
|
||||||
alert("Semua item berhasil dipindahkan ke Brankas")
|
headers: {
|
||||||
closeConfirmModal()
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
location.reload()
|
},
|
||||||
|
});
|
||||||
|
alert("Semua item berhasil dipindahkan ke Brankas");
|
||||||
|
closeConfirmModal();
|
||||||
|
location.reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
alert("Gagal mengosongkan nampan")
|
alert("Gagal mengosongkan nampan");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const editTray = (tray) => {
|
const editTray = (tray) => {
|
||||||
trayName.value = tray.nama
|
trayName.value = tray.nama;
|
||||||
editingTrayId.value = tray.id
|
editingTrayId.value = tray.id;
|
||||||
showModal.value = true
|
showModal.value = true;
|
||||||
}
|
};
|
||||||
|
|
||||||
const deleteTray = async (id) => {
|
const deleteTray = async (id) => {
|
||||||
if (!confirm("Yakin ingin menghapus nampan ini?")) return
|
if (!confirm("Yakin ingin menghapus nampan ini?")) return;
|
||||||
try {
|
try {
|
||||||
await axios.delete(`/api/nampan/${id}`)
|
await axios.delete(`/api/nampan/${id}`, {
|
||||||
alert("Nampan berhasil dihapus")
|
headers: {
|
||||||
location.reload()
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
alert("Nampan berhasil dihapus");
|
||||||
|
location.reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
alert("Gagal menghapus nampan")
|
alert("Gagal menghapus nampan");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,74 +1,124 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
import Home from '../pages/Home.vue'
|
|
||||||
import Produk from '../pages/Produk.vue'
|
|
||||||
import Brankas from '../pages/Brankas.vue'
|
|
||||||
import Tray from '../pages/Tray.vue'
|
|
||||||
import Kasir from '../pages/Kasir.vue'
|
|
||||||
import InputProduk from '../pages/InputProduk.vue'
|
|
||||||
import Kategori from '../pages/Kategori.vue'
|
|
||||||
import Sales from '../pages/Sales.vue'
|
|
||||||
import EditProduk from '../pages/EditProduk.vue'
|
|
||||||
import Akun from '../pages/Akun.vue'
|
|
||||||
|
|
||||||
|
import Produk from "../pages/Produk.vue";
|
||||||
|
import Brankas from "../pages/Brankas.vue";
|
||||||
|
import Tray from "../pages/Tray.vue";
|
||||||
|
import Kasir from "../pages/Kasir.vue";
|
||||||
|
import InputProduk from "../pages/InputProduk.vue";
|
||||||
|
import Kategori from "../pages/Kategori.vue";
|
||||||
|
import Sales from "../pages/Sales.vue";
|
||||||
|
import EditProduk from "../pages/EditProduk.vue";
|
||||||
|
import Laporan from "../pages/Laporan.vue";
|
||||||
|
import Login from "../pages/Login.vue";
|
||||||
|
import Akun from "../pages/Akun.vue";
|
||||||
|
|
||||||
|
import auth from "../middlewares/auth";
|
||||||
|
import guest from "../middlewares/guest";
|
||||||
|
import owner from "../middlewares/owner";
|
||||||
|
|
||||||
|
const middlewareMap = { auth, guest, owner };
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: "/",
|
||||||
name: 'Home',
|
name: "Login",
|
||||||
component: Home
|
component: Login,
|
||||||
|
meta: { middleware: "guest" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/produk',
|
path: "/produk",
|
||||||
name: 'Produk',
|
name: "Produk",
|
||||||
component: Produk
|
component: Produk,
|
||||||
|
meta: { middleware: "auth" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/produk/baru',
|
path: "/produk/baru",
|
||||||
name: 'ProdukBaru',
|
name: "ProdukBaru",
|
||||||
component: InputProduk
|
component: InputProduk,
|
||||||
|
meta: { middleware: ["auth", "owner"] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/brankas',
|
path: "/produk/:id/edit",
|
||||||
name: 'Brankas',
|
name: "EditProduk",
|
||||||
component: Brankas
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/nampan',
|
|
||||||
name: 'Nampan',
|
|
||||||
component: Tray
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/kasir',
|
|
||||||
name: 'Kasir',
|
|
||||||
component: Kasir
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/kategori',
|
|
||||||
name: 'Kategori',
|
|
||||||
component: Kategori
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/sales',
|
|
||||||
name: 'Sales',
|
|
||||||
component: Sales
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/akun',
|
|
||||||
name: 'Akun',
|
|
||||||
component: Akun
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/produk/:id/edit', // :id = parameter dinamis
|
|
||||||
name: 'EditProduk',
|
|
||||||
component: EditProduk,
|
component: EditProduk,
|
||||||
props: true // biar id bisa langsung jadi props di komponen
|
props: true,
|
||||||
}
|
meta: { middleware: ["auth", "owner"] },
|
||||||
]
|
},
|
||||||
|
{
|
||||||
|
path: "/brankas",
|
||||||
|
name: "Brankas",
|
||||||
|
component: Brankas,
|
||||||
|
meta: { middleware: "auth" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/nampan",
|
||||||
|
name: "Nampan",
|
||||||
|
component: Tray,
|
||||||
|
meta: { middleware: "auth" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/sales",
|
||||||
|
name: "Sales",
|
||||||
|
component: Sales,
|
||||||
|
meta: { middleware: "auth" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/kategori",
|
||||||
|
name: "Kategori",
|
||||||
|
component: Kategori,
|
||||||
|
meta: { middleware: "auth" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/kasir",
|
||||||
|
name: "Kasir",
|
||||||
|
component: Kasir,
|
||||||
|
meta: { middleware: "auth" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/laporan",
|
||||||
|
name: "Laporan",
|
||||||
|
component: Laporan,
|
||||||
|
meta: { middleware: ["auth", "owner"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/akun",
|
||||||
|
name: "Akun",
|
||||||
|
component: Akun,
|
||||||
|
meta: { middleware: ["auth", "owner"] },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes
|
routes,
|
||||||
})
|
});
|
||||||
|
|
||||||
export default router
|
router.beforeEach((to, from, next) => {
|
||||||
|
let middlewares = to.meta.middleware;
|
||||||
|
if (!middlewares) return next();
|
||||||
|
|
||||||
|
if (!Array.isArray(middlewares)) {
|
||||||
|
middlewares = [middlewares];
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
const run = () => {
|
||||||
|
const name = middlewares[index];
|
||||||
|
const mw = middlewareMap[name];
|
||||||
|
if (!mw) return next();
|
||||||
|
|
||||||
|
mw(to, from, (redirect) => {
|
||||||
|
if (redirect) return next(redirect);
|
||||||
|
index++;
|
||||||
|
if (index < middlewares.length) {
|
||||||
|
run();
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
run();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
@ -1,12 +1,34 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable-no" />
|
|
||||||
<title>Abbauf App</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<title>@yield('title', config('app.name', 'Abbauf App'))</title>
|
||||||
|
|
||||||
|
<meta name="description" content="@yield('description', 'Deskripsi default untuk aplikasi Abbauf.')">
|
||||||
|
<meta name="author" content="Nama Anda atau Perusahaan Anda">
|
||||||
|
|
||||||
|
<meta property="og:title" content="@yield('title', config('app.name', 'Abbauf App'))" />
|
||||||
|
<meta property="og:description" content="@yield('description', 'Deskripsi default untuk aplikasi Abbauf.')" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" content="{{ url()->current() }}" />
|
||||||
|
<meta property="og:image" content="@yield('og_image', asset('images/default-social-image.jpg'))" />
|
||||||
|
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="@yield('title', config('app.name', 'Abbauf App'))">
|
||||||
|
<meta name="twitter:description" content="@yield('description', 'Deskripsi default untuk aplikasi Abbauf.')">
|
||||||
|
<meta name="twitter:image" content="@yield('og_image', asset('images/default-social-image.jpg'))">
|
||||||
|
|
||||||
|
<link rel="icon" href="{{ asset('favicon.ico') }}" type="image/x-icon">
|
||||||
|
<link rel="apple-touch-icon" href="{{ asset('apple-touch-icon.png') }}">
|
||||||
|
|
||||||
|
<meta name="theme-color" content="#FFFFFF">
|
||||||
|
|
||||||
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
</body>
|
</body>
|
||||||
@vite(['resources/js/app.js', 'resources/css/app.css'])
|
|
||||||
</html>
|
</html>
|
@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\AuthController;
|
||||||
use App\Http\Controllers\FotoSementaraController;
|
use App\Http\Controllers\FotoSementaraController;
|
||||||
use App\Http\Controllers\ItemController;
|
use App\Http\Controllers\ItemController;
|
||||||
use App\Http\Controllers\KategoriController;
|
use App\Http\Controllers\KategoriController;
|
||||||
@ -8,19 +9,29 @@ use App\Http\Controllers\ProdukController;
|
|||||||
use App\Http\Controllers\SalesController;
|
use App\Http\Controllers\SalesController;
|
||||||
use App\Http\Controllers\UserController;
|
use App\Http\Controllers\UserController;
|
||||||
use App\Http\Controllers\TransaksiController;
|
use App\Http\Controllers\TransaksiController;
|
||||||
|
use App\Http\Controllers\LaporanController;
|
||||||
|
use App\Models\Kategori;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
// Backend API
|
|
||||||
Route::prefix('api')->group(function () {
|
|
||||||
Route::apiResource('nampan', NampanController::class);
|
|
||||||
Route::apiResource('produk', ProdukController::class);
|
|
||||||
Route::apiResource('item', ItemController::class);
|
|
||||||
Route::apiResource('sales', SalesController::class);
|
|
||||||
Route::apiResource('user', UserController::class);
|
|
||||||
Route::apiResource('transaksi', TransaksiController::class);
|
|
||||||
Route::apiResource('kategori', KategoriController::class);
|
|
||||||
|
|
||||||
Route::get('brankas', [ItemController::class, 'brankasItem']);
|
// ============================
|
||||||
|
// Backend API
|
||||||
|
// ============================
|
||||||
|
Route::prefix('api')->group(function () {
|
||||||
|
// Auth
|
||||||
|
Route::post('/login', [AuthController::class, 'login'])->middleware('guest')->name('login');
|
||||||
|
Route::post('/logout', [AuthController::class, 'logout'])->middleware('auth:sanctum')->name('logout');
|
||||||
|
|
||||||
|
Route::middleware(['auth:sanctum', 'role:owner'])->group(function () {
|
||||||
|
Route::apiResource('nampan', NampanController::class)->except(['index', 'show']);
|
||||||
|
Route::apiResource('produk', ProdukController::class)->except(['index', 'show']);
|
||||||
|
Route::apiResource('item', ItemController::class)->except(['index', 'show']);
|
||||||
|
Route::apiResource('sales', SalesController::class)->except(['index', 'show']);
|
||||||
|
Route::apiResource('kategori', KategoriController::class)->except(['index', 'show']);
|
||||||
|
Route::apiResource('user', UserController::class);
|
||||||
|
|
||||||
|
// Custom Endpoint
|
||||||
|
|
||||||
Route::delete('kosongkan-nampan', [NampanController::class, 'kosongkan']);
|
Route::delete('kosongkan-nampan', [NampanController::class, 'kosongkan']);
|
||||||
|
|
||||||
// Foto Sementara
|
// Foto Sementara
|
||||||
@ -28,9 +39,33 @@ Route::prefix('api')->group(function () {
|
|||||||
Route::delete('foto/hapus/{id}', [FotoSementaraController::class, 'hapus']);
|
Route::delete('foto/hapus/{id}', [FotoSementaraController::class, 'hapus']);
|
||||||
Route::get('foto/{user_id}', [FotoSementaraController::class, 'getAll']);
|
Route::get('foto/{user_id}', [FotoSementaraController::class, 'getAll']);
|
||||||
Route::delete('foto/reset/{user_id}', [FotoSementaraController::class, 'reset']);
|
Route::delete('foto/reset/{user_id}', [FotoSementaraController::class, 'reset']);
|
||||||
|
|
||||||
|
// Laporan
|
||||||
|
Route::get('laporan', [LaporanController::class, 'ringkasan']);
|
||||||
|
Route::get('detail-laporan', [LaporanController::class, 'detail']);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Frontend SPA
|
Route::middleware(['auth:sanctum', 'role:owner,kasir'])->group(function () {
|
||||||
|
Route::apiResource('transaksi', TransaksiController::class);
|
||||||
|
Route::get('produk', [ProdukController::class, 'index']);
|
||||||
|
Route::get('produk/{id}', [ProdukController::class, 'show']);
|
||||||
|
Route::get('nampan', [NampanController::class, 'index']);
|
||||||
|
Route::get('nampan/{id}', [NampanController::class, 'show']);
|
||||||
|
Route::get('item', [ItemController::class, 'index']);
|
||||||
|
Route::get('item/{id}', [ItemController::class, 'show']);
|
||||||
|
Route::get('sales', [SalesController::class, 'index']);
|
||||||
|
Route::get('sales/{id}', [SalesController::class, 'show']);
|
||||||
|
Route::get('kategori', [KategoriController::class, 'index']);
|
||||||
|
Route::get('kategori/{id}', [KategoriController::class, 'show']);
|
||||||
|
Route::get('brankas', [ItemController::class, 'brankasItem']);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// Frontend SPA (Vue / React dll.)
|
||||||
|
// ============================
|
||||||
Route::get('/{any}', function () {
|
Route::get('/{any}', function () {
|
||||||
return view('app');
|
return view('app');
|
||||||
})->where('any', '^(?!storage|api).*$');
|
})->where('any', '^(?!storage|api).*$');
|
Loading…
Reference in New Issue
Block a user