Bläddra i källkod

fix: purchase master

Muhammad Iqbal Afandi 3 år sedan
förälder
incheckning
f35351c35d

+ 27
- 10
app/Http/Controllers/PurchaseController.php Visa fil

@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
5 5
 use App\Http\Requests\Purchase\StorePurchaseRequest;
6 6
 use App\Http\Requests\Purchase\UpdatePurchaseRequest;
7 7
 use App\Models\Ppn;
8
+use App\Models\Price;
8 9
 use App\Models\Product;
9 10
 use App\Models\Purchase;
10 11
 use App\Models\StockProduct;
@@ -36,13 +37,11 @@ class PurchaseController extends Controller
36 37
                 ->through(fn($purchase) => [
37 38
                     'id' => $purchase->id,
38 39
                     'updatedAt' => $purchase->updated_at,
39
-                    'number' => $purchase->number,
40
-                    'status' => $purchase->status,
41
-                    'price' => $purchase->purchaseDetail->price,
42
-                    'ppn' => $purchase->purchaseDetail->ppn,
43
-                    'qty' => $purchase->purchaseDetail->qty,
44
-                    'productName' => $purchase->product->name,
45
-                    'productNumber' => $purchase->product->number
40
+                    'email' => $purchase->supplier->email,
41
+                    'name' => $purchase->supplier->name,
42
+                    'phone' => $purchase->supplier->phone,
43
+                    'price' => $purchase->totalPrice,
44
+                    'status' => $purchase->status
46 45
                 ])
47 46
         ]);
48 47
     }
@@ -82,14 +81,32 @@ class PurchaseController extends Controller
82 81
         DB::beginTransaction();
83 82
 
84 83
         try {
84
+            $ppn = Ppn::first()->getRawOriginal('ppn');
85
+
85 86
             $validated = $request->safe()->merge([
86
-                'user_id' => auth()->user()->id,
87
-                'ppn' => Ppn::first()->getRawOriginal('ppn')
87
+                'user_id' => auth()->user()->id
88 88
             ])->all();
89 89
 
90 90
             $purchase = Purchase::create($validated);
91 91
 
92
-            $purchase->purchaseDetail()->create($validated);
92
+            foreach ($request->products as $product) {
93
+                $validated = $request->safe()->merge([
94
+                    'product_number' => $product['number'],
95
+                    'name' => $product['name'],
96
+                    'price' => $product['price'],
97
+                    'qty' => $product['qty'],
98
+                    'ppn' => $ppn
99
+                ])->all();
100
+
101
+                $purchase->purchaseDetail()->create($validated);
102
+
103
+                $validated = $request->safe()->merge([
104
+                    'product_number' => $product['number'],
105
+                    'price' => $product['price'] + $product['price'] * ($ppn / 100)
106
+                ])->all();
107
+
108
+                Price::create($validated);
109
+            }
93 110
 
94 111
             DB::commit();
95 112
 

+ 1
- 3
app/Http/Requests/Purchase/StorePurchaseRequest.php Visa fil

@@ -26,10 +26,8 @@ class StorePurchaseRequest extends FormRequest
26 26
         return [
27 27
             'number' => 'required|string|unique:sales,number',
28 28
             'status' => 'required|string',
29
-            'price' => 'required|numeric',
30
-            'qty' => 'required|numeric',
31 29
             'supplier_id' => 'required|numeric',
32
-            'product_number' => 'required|string'
30
+            'products' => 'required'
33 31
         ];
34 32
     }
35 33
 }

+ 9
- 2
app/Models/Purchase.php Visa fil

@@ -27,7 +27,7 @@ class Purchase extends Model
27 27
 
28 28
     public function purchaseDetail()
29 29
     {
30
-        return $this->hasOne(PurchaseDetail::class, 'purchase_number', 'number');
30
+        return $this->hasMany(PurchaseDetail::class, 'purchase_number', 'number');
31 31
     }
32 32
 
33 33
     public function product()
@@ -51,9 +51,16 @@ class Purchase extends Model
51 51
     {
52 52
         $query->when($filters['search'] ?? null, function ($query, $search) {
53 53
             $query->where(function ($query) use ($search) {
54
-                $query->where('number', 'like', '%' . $search . '%')
54
+                $query->where('name', 'like', '%' . $search . '%')
55
+                    ->orWhere('phone', 'like', '%' . $search . '%')
56
+                    ->orWhere('email', 'like', '%' . $search . '%')
55 57
                     ->orWhere('status', 'like', '%' . $search . '%');
56 58
             });
57 59
         });
58 60
     }
61
+
62
+    public function totalPrice()
63
+    {
64
+        return $this->purchaseDetail();
65
+    }
59 66
 }

+ 1
- 1
designs/diagrams/database.puml Visa fil

@@ -122,7 +122,7 @@ Customer ||--|{ Sale
122 122
 Customer ||--|| Price
123 123
 Sale ||--|| SaleDetail
124 124
 Sale ||--|| Price
125
-Purchase ||--|| PurchaseDetail
125
+Purchase }|--|| PurchaseDetail
126 126
 Purchase ||--|| Price
127 127
 Purchase ||--|{ StockProduct
128 128
 SaleDetail ||--|{ Product

Binär
designs/diagrams/database/Database.png Visa fil


+ 3
- 3
designs/diagrams/database/Database.svg Visa fil

@@ -20,14 +20,14 @@ link Customer to Sale--><g id="link_Customer_Sale"><path codeLine="120" d="M171.
20 20
 link Customer to Price--><g id="link_Customer_Price"><path codeLine="121" d="M127.01,154.51 C81.44,287.23 6,578.11 142.68,757 C182.72,809.4 250.84,842.97 302.95,862.36 " fill="none" id="Customer-Price" style="stroke:#181818;stroke-width:1.0;"/><line style="stroke:#181818;stroke-width:1.0;" x1="307.1648" x2="304.406" y1="859.626" y2="867.1352"/><line style="stroke:#181818;stroke-width:1.0;" x1="304.3488" x2="301.59" y1="858.5914" y2="866.1006"/><line style="stroke:#181818;stroke-width:1.0;" x1="302.0307" x2="309.54" y1="862.0012" y2="864.76"/><line style="stroke:#181818;stroke-width:1.0;" x1="124.3429" x2="131.9041" y1="150.0141" y2="152.6271"/><line style="stroke:#181818;stroke-width:1.0;" x1="123.363" x2="130.9243" y1="152.8496" y2="155.4626"/><line style="stroke:#181818;stroke-width:1.0;" x1="126.817" x2="129.43" y1="155.1012" y2="147.54"/></g><!--MD5=[1b8181cc673ea47d6cea814c6d86a94d]
21 21
 link Sale to SaleDetail--><g id="link_Sale_SaleDetail"><path codeLine="122" d="M203.68,363.39 C203.68,378.07 203.68,393.56 203.68,408.5 " fill="none" id="Sale-SaleDetail" style="stroke:#181818;stroke-width:1.0;"/><line style="stroke:#181818;stroke-width:1.0;" x1="207.68" x2="199.68" y1="411.79" y2="411.79"/><line style="stroke:#181818;stroke-width:1.0;" x1="207.68" x2="199.68" y1="408.79" y2="408.79"/><line style="stroke:#181818;stroke-width:1.0;" x1="203.68" x2="203.68" y1="407.79" y2="415.79"/><line style="stroke:#181818;stroke-width:1.0;" x1="199.68" x2="207.68" y1="360.15" y2="360.15"/><line style="stroke:#181818;stroke-width:1.0;" x1="199.68" x2="207.68" y1="363.15" y2="363.15"/><line style="stroke:#181818;stroke-width:1.0;" x1="203.68" x2="203.68" y1="364.15" y2="356.15"/></g><!--MD5=[fd58fb4015992d940e1d5ff8549f8c35]
22 22
 link Sale to Price--><g id="link_Sale_Price"><path codeLine="123" d="M148.25,358.24 C136.93,376.07 126.66,395.93 120.68,416 C101.28,481.17 98.05,504.88 120.68,569 C156.96,671.77 242.55,765.67 304.36,823.7 " fill="none" id="Sale-Price" style="stroke:#181818;stroke-width:1.0;"/><line style="stroke:#181818;stroke-width:1.0;" x1="309.3239" x2="303.8665" y1="822.8565" y2="828.7061"/><line style="stroke:#181818;stroke-width:1.0;" x1="307.1304" x2="301.673" y1="820.81" y2="826.6596"/><line style="stroke:#181818;stroke-width:1.0;" x1="303.6705" x2="309.52" y1="823.0526" y2="828.51"/><line style="stroke:#181818;stroke-width:1.0;" x1="146.5966" x2="153.3052" y1="353.4452" y2="357.8034"/><line style="stroke:#181818;stroke-width:1.0;" x1="144.9623" x2="151.6709" y1="355.961" y2="360.3192"/><line style="stroke:#181818;stroke-width:1.0;" x1="147.7718" x2="152.13" y1="358.9787" y2="352.27"/></g><!--MD5=[c0e5a52b16fbcef22495159b374d909e]
23
-link Purchase to PurchaseDetail--><g id="link_Purchase_PurchaseDetail"><path codeLine="124" d="M550.68,363.39 C550.68,378.07 550.68,393.56 550.68,408.5 " fill="none" id="Purchase-PurchaseDetail" style="stroke:#181818;stroke-width:1.0;"/><line style="stroke:#181818;stroke-width:1.0;" x1="554.68" x2="546.68" y1="411.79" y2="411.79"/><line style="stroke:#181818;stroke-width:1.0;" x1="554.68" x2="546.68" y1="408.79" y2="408.79"/><line style="stroke:#181818;stroke-width:1.0;" x1="550.68" x2="550.68" y1="407.79" y2="415.79"/><line style="stroke:#181818;stroke-width:1.0;" x1="546.68" x2="554.68" y1="360.15" y2="360.15"/><line style="stroke:#181818;stroke-width:1.0;" x1="546.68" x2="554.68" y1="363.15" y2="363.15"/><line style="stroke:#181818;stroke-width:1.0;" x1="550.68" x2="550.68" y1="364.15" y2="356.15"/></g><!--MD5=[9aab0b89b89995cf669aab5ed34799da]
23
+link Purchase to PurchaseDetail--><g id="link_Purchase_PurchaseDetail"><path codeLine="124" d="M550.68,364.31 C550.68,378.37 550.68,393.14 550.68,407.43 " fill="none" id="Purchase-PurchaseDetail" style="stroke:#181818;stroke-width:1.0;"/><line style="stroke:#181818;stroke-width:1.0;" x1="554.68" x2="546.68" y1="411.79" y2="411.79"/><line style="stroke:#181818;stroke-width:1.0;" x1="554.68" x2="546.68" y1="408.79" y2="408.79"/><line style="stroke:#181818;stroke-width:1.0;" x1="550.68" x2="550.68" y1="407.79" y2="415.79"/><line style="stroke:#181818;stroke-width:1.0;" x1="550.68" x2="544.68" y1="364.15" y2="356.15"/><line style="stroke:#181818;stroke-width:1.0;" x1="550.68" x2="556.68" y1="364.15" y2="356.15"/><line style="stroke:#181818;stroke-width:1.0;" x1="550.68" x2="550.68" y1="364.15" y2="356.15"/><line style="stroke:#181818;stroke-width:1.0;" x1="546.68" x2="554.68" y1="366.15" y2="366.15"/></g><!--MD5=[9aab0b89b89995cf669aab5ed34799da]
24 24
 link Purchase to Price--><g id="link_Purchase_Price"><path codeLine="125" d="M602.67,348.16 C617.24,368.32 631.15,391.88 638.68,416 C658.97,480.9 661.92,505.09 638.68,569 C600.38,674.34 509.68,769.01 445.31,826.44 " fill="none" id="Purchase-Price" style="stroke:#181818;stroke-width:1.0;"/><line style="stroke:#181818;stroke-width:1.0;" x1="445.5965" x2="440.2927" y1="831.5327" y2="825.5435"/><line style="stroke:#181818;stroke-width:1.0;" x1="447.8424" x2="442.5386" y1="829.5438" y2="823.5546"/><line style="stroke:#181818;stroke-width:1.0;" x1="445.9392" x2="439.95" y1="825.8862" y2="831.19"/><line style="stroke:#181818;stroke-width:1.0;" x1="597.4989" x2="603.934" y1="347.894" y2="343.1411"/><line style="stroke:#181818;stroke-width:1.0;" x1="599.2812" x2="605.7163" y1="350.3071" y2="345.5543"/><line style="stroke:#181818;stroke-width:1.0;" x1="603.0928" x2="598.34" y1="348.7351" y2="342.3"/></g><!--MD5=[d9346253e56b18202ac3beab23dddd24]
25 25
 link Purchase to StockProduct--><g id="link_Purchase_StockProduct"><path codeLine="126" d="M497.67,350 C479.03,371.45 457.89,395.77 438.46,418.12 " fill="none" id="Purchase-StockProduct" style="stroke:#181818;stroke-width:1.0;"/><line style="stroke:#181818;stroke-width:1.0;" x1="438.2727" x2="437.562" y1="418.3273" y2="428.302"/><line style="stroke:#181818;stroke-width:1.0;" x1="438.2727" x2="428.498" y1="418.3273" y2="420.438"/><line style="stroke:#181818;stroke-width:1.0;" x1="438.2727" x2="433.03" y1="418.3273" y2="424.37"/><line style="stroke:#181818;stroke-width:1.0;" x1="442.6047" x2="436.562" y1="419.438" y2="414.1953"/><line style="stroke:#181818;stroke-width:1.0;" x1="497.4168" x2="503.4534" y1="344.1934" y2="349.4432"/><line style="stroke:#181818;stroke-width:1.0;" x1="495.4482" x2="501.4847" y1="346.4571" y2="351.7069"/><line style="stroke:#181818;stroke-width:1.0;" x1="497.8102" x2="503.06" y1="349.8365" y2="343.8"/></g><!--MD5=[cc8c2cf61c441007fd82fc98c1838a15]
26 26
 link SaleDetail to Product--><g id="link_SaleDetail_Product"><path codeLine="127" d="M274.24,575.41 C292.6,596.72 311.91,619.13 328.64,638.55 " fill="none" id="SaleDetail-Product" style="stroke:#181818;stroke-width:1.0;"/><line style="stroke:#181818;stroke-width:1.0;" x1="328.7678" x2="338.5318" y1="638.681" y2="640.8408"/><line style="stroke:#181818;stroke-width:1.0;" x1="328.7678" x2="329.4282" y1="638.681" y2="648.6592"/><line style="stroke:#181818;stroke-width:1.0;" x1="328.7678" x2="333.98" y1="638.681" y2="644.75"/><line style="stroke:#181818;stroke-width:1.0;" x1="330.4992" x2="324.4302" y1="634.5576" y2="639.7698"/><line style="stroke:#181818;stroke-width:1.0;" x1="268.4058" x2="274.4617" y1="574.7517" y2="569.5242"/><line style="stroke:#181818;stroke-width:1.0;" x1="270.3661" x2="276.422" y1="577.0226" y2="571.7951"/><line style="stroke:#181818;stroke-width:1.0;" x1="274.0475" x2="268.82" y1="575.1659" y2="569.11"/></g><!--MD5=[5a75ef0720b826a25eafd7a682d30a1b]
27 27
 link Sale to StockProduct--><g id="link_Sale_StockProduct"><path codeLine="128" d="M260.62,356.42 C277.31,376.19 295.65,397.91 312.65,418.04 " fill="none" id="Sale-StockProduct" style="stroke:#181818;stroke-width:1.0;"/><line style="stroke:#181818;stroke-width:1.0;" x1="312.7221" x2="322.4664" y1="418.1148" y2="420.3616"/><line style="stroke:#181818;stroke-width:1.0;" x1="312.7221" x2="313.2936" y1="418.1148" y2="428.0984"/><line style="stroke:#181818;stroke-width:1.0;" x1="312.7221" x2="317.88" y1="418.1148" y2="424.23"/><line style="stroke:#181818;stroke-width:1.0;" x1="314.4902" x2="308.375" y1="414.007" y2="419.1649"/><line style="stroke:#181818;stroke-width:1.0;" x1="254.7414" x2="260.8566" y1="355.6666" y2="350.5086"/><line style="stroke:#181818;stroke-width:1.0;" x1="256.6756" x2="262.7908" y1="357.9598" y2="352.8018"/><line style="stroke:#181818;stroke-width:1.0;" x1="260.3779" x2="255.22" y1="356.1452" y2="350.03"/></g><!--MD5=[636c4cbf2bb40ce634cacaf01a62e11c]
28 28
 link PurchaseDetail to Product--><g id="link_PurchaseDetail_Product"><path codeLine="129" d="M478.21,575.23 C458.75,597.19 438.24,620.32 420.65,640.16 " fill="none" id="PurchaseDetail-Product" style="stroke:#181818;stroke-width:1.0;"/><line style="stroke:#181818;stroke-width:1.0;" x1="420.5934" x2="419.7821" y1="640.2105" y2="650.1775"/><line style="stroke:#181818;stroke-width:1.0;" x1="420.5934" x2="410.7979" y1="640.2105" y2="642.2225"/><line style="stroke:#181818;stroke-width:1.0;" x1="420.5934" x2="415.29" y1="640.2105" y2="646.2"/><line style="stroke:#181818;stroke-width:1.0;" x1="424.914" x2="418.9244" y1="641.3648" y2="636.0614"/><line style="stroke:#181818;stroke-width:1.0;" x1="477.9936" x2="483.9831" y1="569.4531" y2="574.7564"/><line style="stroke:#181818;stroke-width:1.0;" x1="476.0048" x2="481.9943" y1="571.6991" y2="577.0025"/><line style="stroke:#181818;stroke-width:1.0;" x1="478.3366" x2="483.64" y1="575.0995" y2="569.11"/></g><!--MD5=[469537f6e6fbfad22558102ade648a2e]
29 29
 link StockProduct to Product--><g id="link_StockProduct_Product"><path codeLine="130" d="M374.68,568.81 C374.68,585.83 374.68,603.86 374.68,620.72 " fill="none" id="StockProduct-Product" style="stroke:#181818;stroke-width:1.0;"/><line style="stroke:#181818;stroke-width:1.0;" x1="374.68" x2="380.68" y1="620.89" y2="628.89"/><line style="stroke:#181818;stroke-width:1.0;" x1="374.68" x2="368.68" y1="620.89" y2="628.89"/><line style="stroke:#181818;stroke-width:1.0;" x1="374.68" x2="374.68" y1="620.89" y2="628.89"/><line style="stroke:#181818;stroke-width:1.0;" x1="378.68" x2="370.68" y1="618.89" y2="618.89"/><line style="stroke:#181818;stroke-width:1.0;" x1="370.68" x2="378.68" y1="564.7" y2="564.7"/><line style="stroke:#181818;stroke-width:1.0;" x1="370.68" x2="378.68" y1="567.7" y2="567.7"/><line style="stroke:#181818;stroke-width:1.0;" x1="374.68" x2="374.68" y1="568.7" y2="560.7"/></g><!--MD5=[5fa2e163e1a5922a0d0ebcb119c05bb7]
30
-link Product to Price--><g id="link_Product_Price"><path codeLine="131" d="M374.68,764.3 C374.68,779.09 374.68,794.73 374.68,809.66 " fill="none" id="Product-Price" style="stroke:#181818;stroke-width:1.0;"/><line style="stroke:#181818;stroke-width:1.0;" x1="378.68" x2="370.68" y1="812.93" y2="812.93"/><line style="stroke:#181818;stroke-width:1.0;" x1="378.68" x2="370.68" y1="809.93" y2="809.93"/><line style="stroke:#181818;stroke-width:1.0;" x1="374.68" x2="374.68" y1="808.93" y2="816.93"/><line style="stroke:#181818;stroke-width:1.0;" x1="370.68" x2="378.68" y1="761" y2="761"/><line style="stroke:#181818;stroke-width:1.0;" x1="370.68" x2="378.68" y1="764" y2="764"/><line style="stroke:#181818;stroke-width:1.0;" x1="374.68" x2="374.68" y1="765" y2="757"/></g><!--MD5=[64b2795114cf89c88947b63cdc960ebf]
30
+link Product to Price--><g id="link_Product_Price"><path codeLine="131" d="M374.68,764.3 C374.68,779.09 374.68,794.73 374.68,809.66 " fill="none" id="Product-Price" style="stroke:#181818;stroke-width:1.0;"/><line style="stroke:#181818;stroke-width:1.0;" x1="378.68" x2="370.68" y1="812.93" y2="812.93"/><line style="stroke:#181818;stroke-width:1.0;" x1="378.68" x2="370.68" y1="809.93" y2="809.93"/><line style="stroke:#181818;stroke-width:1.0;" x1="374.68" x2="374.68" y1="808.93" y2="816.93"/><line style="stroke:#181818;stroke-width:1.0;" x1="370.68" x2="378.68" y1="761" y2="761"/><line style="stroke:#181818;stroke-width:1.0;" x1="370.68" x2="378.68" y1="764" y2="764"/><line style="stroke:#181818;stroke-width:1.0;" x1="374.68" x2="374.68" y1="765" y2="757"/></g><!--MD5=[6ccd130684db3a325492519a433f01fa]
31 31
 @startuml Database
32 32
 
33 33
 entity User {
@@ -152,7 +152,7 @@ Customer ||- -|{ Sale
152 152
 Customer ||- -|| Price
153 153
 Sale ||- -|| SaleDetail
154 154
 Sale ||- -|| Price
155
-Purchase ||- -|| PurchaseDetail
155
+Purchase }|- -|| PurchaseDetail
156 156
 Purchase ||- -|| Price
157 157
 Purchase ||- -|{ StockProduct
158 158
 SaleDetail ||- -|{ Product

+ 6
- 5
resources/js/components/AppAutoComplete.vue Visa fil

@@ -23,6 +23,10 @@ const props = defineProps({
23 23
     type: String,
24 24
     required: true,
25 25
   },
26
+  empty: {
27
+    type: Boolean,
28
+    default: false,
29
+  },
26 30
   refreshData: {
27 31
     type: String,
28 32
     required: true,
@@ -87,7 +91,7 @@ const onSelect = (event) => {
87 91
         v-if="error"
88 92
         class="mt-1"
89 93
         :class="{
90
-          'mb-2': suggestions.length === 0 || modelValue.length === 0,
94
+          'mb-2': empty,
91 95
           'p-error': isError,
92 96
         }"
93 97
         :id="ariaDescribedbyLabel"
@@ -95,10 +99,7 @@ const onSelect = (event) => {
95 99
         {{ error }}
96 100
       </small>
97 101
 
98
-      <small
99
-        v-if="suggestions.length === 0 || modelValue.length === 0"
100
-        class="mt-1"
101
-      >
102
+      <small v-if="empty" class="mt-1">
102 103
         <slot name="empty" />
103 104
       </small>
104 105
     </div>

+ 4
- 1
resources/js/components/AppSearch.vue Visa fil

@@ -23,5 +23,8 @@ watch(search, () => {
23 23
 </script>
24 24
 
25 25
 <template>
26
-  <InputText v-model="search" />
26
+  <div class="flex align-items-center gap-3">
27
+    <InputText v-bind="$attrs" v-model="search" />
28
+    <i class="pi pi-search" />
29
+  </div>
27 30
 </template>

+ 13
- 1
resources/js/pages/Purchases/Components/Cart.vue Visa fil

@@ -1,4 +1,6 @@
1 1
 <script setup>
2
+import { IDRCurrencyFormat } from '@/utils/currencyFormat'
3
+
2 4
 defineProps({
3 5
   title: String,
4 6
   headerTable: {
@@ -29,7 +31,17 @@ defineProps({
29 31
       :field="value.field"
30 32
       :header="value.header"
31 33
       :key="value.field"
32
-    />
34
+    >
35
+      <template #body="{ data, field }">
36
+        <template v-if="field === 'price'">
37
+          {{ IDRCurrencyFormat(data[field]) }}
38
+        </template>
39
+
40
+        <template v-else-if="field === 'ppn'"> {{ data[field] }}% </template>
41
+
42
+        <template v-else> {{ data[field] }} </template>
43
+      </template>
44
+    </Column>
33 45
 
34 46
     <Column>
35 47
       <template #body="{ index }">

+ 13
- 26
resources/js/pages/Purchases/Components/Details.vue Visa fil

@@ -5,16 +5,16 @@ defineProps({
5 5
   title: String,
6 6
   number: String,
7 7
   price: Number,
8
-  qty: String,
9 8
   ppn: Number,
10 9
   status: String,
11
-  person: Object,
10
+  person: null,
11
+  message: String,
12 12
   disabled: Boolean,
13 13
 })
14 14
 </script>
15 15
 
16 16
 <template>
17
-  <Card>
17
+  <Card class="bg-primary">
18 18
     <template #title>
19 19
       <h2 class="text-2xl font-bold">{{ title }}</h2>
20 20
     </template>
@@ -36,14 +36,7 @@ defineProps({
36 36
           </div>
37 37
         </div>
38 38
 
39
-        <div
40
-          v-if="
41
-            person !== null &&
42
-            typeof person === 'object' &&
43
-            Object.keys(person).length
44
-          "
45
-          class="col-12"
46
-        >
39
+        <div v-if="person?.id" class="col-12">
47 40
           <div class="grid">
48 41
             <div class="col">
49 42
               <h3 class="text-base">Nama</h3>
@@ -70,19 +63,9 @@ defineProps({
70 63
         <Divider type="dashed" />
71 64
         <div class="col-12">
72 65
           <div class="grid">
73
-            <div class="col">
66
+            <div v-if="price" class="col">
74 67
               <h3 class="text-base">Harga</h3>
75
-              <span v-if="price">{{ IDRCurrencyFormat(price) }}</span>
76
-            </div>
77
-
78
-            <div class="col">
79
-              <h3 class="text-base">Kuantitas</h3>
80
-              <span>{{ qty }}</span>
81
-            </div>
82
-
83
-            <div class="col">
84
-              <h3 class="text-base">PPN</h3>
85
-              <span>{{ ppn }} %</span>
68
+              <span>{{ IDRCurrencyFormat(price) }}</span>
86 69
             </div>
87 70
           </div>
88 71
         </div>
@@ -90,13 +73,17 @@ defineProps({
90 73
     </template>
91 74
 
92 75
     <template #footer>
93
-      <div class="flex flex-column md:flex-row justify-content-end">
76
+      <div
77
+        class="flex flex-column md:flex-row gap-2 md:gap-0 md:justify-content-between md:align-items-center"
78
+      >
79
+        <small>{{ message }}</small>
80
+
94 81
         <Button
95 82
           label="Simpan"
96 83
           icon="pi pi-check"
97
-          class="p-button-outlined"
84
+          class="p-button-outlined bg-primary-reverse"
98 85
           :disabled="disabled"
99
-          @click="$emit('click')"
86
+          @click="$emit('submit')"
100 87
         />
101 88
       </div>
102 89
     </template>

+ 41
- 0
resources/js/pages/Purchases/Composables/onShowDialog.js Visa fil

@@ -0,0 +1,41 @@
1
+import { useDialog } from 'primevue/usedialog'
2
+import SupplierCreate from '../Components/SupplierCreate.vue'
3
+import ProductCreate from '../Components/ProductCreate.vue'
4
+
5
+export function onShowDialog() {
6
+  const dialog = useDialog()
7
+
8
+  const dialogStyle = {
9
+    style: {
10
+      width: '50vw',
11
+    },
12
+    breakpoints: {
13
+      '960px': '75vw',
14
+      '640px': '90vw',
15
+    },
16
+    modal: true,
17
+  }
18
+
19
+  const onShowCreateSupplier = () => {
20
+    dialog.open(SupplierCreate, {
21
+      props: {
22
+        header: 'Tambah Supplier',
23
+        ...dialogStyle,
24
+      },
25
+    })
26
+  }
27
+
28
+  const onShowCreateProduct = () => {
29
+    dialog.open(ProductCreate, {
30
+      props: {
31
+        header: 'Tambah Produk',
32
+        ...dialogStyle,
33
+      },
34
+    })
35
+  }
36
+
37
+  return {
38
+    onShowCreateProduct,
39
+    onShowCreateSupplier,
40
+  }
41
+}

+ 31
- 16
resources/js/pages/Purchases/Composables/useProductCart.js Visa fil

@@ -1,46 +1,61 @@
1
-import { reactive } from 'vue'
2 1
 import FormValidationError from '@/utils/FormValidationError'
2
+import { reactive } from 'vue'
3 3
 
4 4
 export function useProductCart(form) {
5
-  const cartProduct = reactive([])
6
-
7
-  const productAddValidation = () => {
8
-    if (form.price) {
9
-      throw new FormValidationError('Nilai tidak boleh kosong', 'price')
10
-    } else if (form.qty) {
11
-      throw new FormValidationError('Nilai tidak boleh kosong', 'qty')
12
-    } else if (form.product) {
13
-      throw new FormValidationError('Nilai tidak boleh kosong', 'product')
5
+  const productCart = reactive([])
6
+
7
+  const productValidation = () => {
8
+    const existProduct = productCart.find(
9
+      (product) => product.number === form.product.number
10
+    )
11
+
12
+    if (existProduct) {
13
+      throw new FormValidationError('Produk sudah ada dikeranjang', 'product')
14 14
     }
15 15
   }
16 16
 
17 17
   const onAddProduct = () => {
18 18
     try {
19
-      form.clearErrors(['price', 'qty', 'product'])
19
+      form.clearErrors('product', 'price', 'qty')
20 20
 
21
-      productAddValidation()
21
+      productValidation()
22 22
 
23
-      cartProduct.push({
23
+      productCart.push({
24 24
         number: form.product.number,
25 25
         name: form.product.name,
26 26
         price: form.price,
27 27
         qty: form.qty,
28
+        unit: form.product.unit,
29
+        ppn: form.ppn,
28 30
       })
31
+
32
+      form.reset('product', 'price', 'qty')
29 33
     } catch (e) {
30 34
       form.setError(e.field, e.message)
31 35
     }
32 36
   }
33 37
 
34 38
   const onDeleteProduct = (index) => {
35
-    cartProduct.splice(index)
39
+    productCart.splice(index, 1)
36 40
   }
37 41
 
38
-  const onClearProduct = () => {}
42
+  const onClearProduct = () => {
43
+    productCart.splice(0)
44
+  }
45
+
46
+  const totalProductPrice = () => {
47
+    const productPrices = productCart.map(
48
+      (product) => product.price + product.price * (form.ppn / 100)
49
+    )
50
+
51
+    return productPrices.reduce((prev, current) => prev + current, 0)
52
+  }
39 53
 
40 54
   return {
41
-    cartProduct,
55
+    productCart,
42 56
     onAddProduct,
43 57
     onDeleteProduct,
44 58
     onClearProduct,
59
+    totalProductPrice,
45 60
   }
46 61
 }

+ 181
- 170
resources/js/pages/Purchases/Create.vue Visa fil

@@ -1,12 +1,10 @@
1 1
 <script setup>
2
-import { useDialog } from 'primevue/usedialog'
3
-import { optionStatus, dialogStyle } from './config'
2
+import { optionStatus } from './config'
4 3
 import { cartTable } from './config'
5
-import SupplierCreate from './Components/SupplierCreate.vue'
6
-import ProductCreate from './Components/ProductCreate.vue'
7 4
 import Details from './Components/Details.vue'
8 5
 import Cart from './Components/Cart.vue'
9 6
 import { useProductCart } from './Composables/useProductCart'
7
+import { onShowDialog } from './Composables/onShowDialog'
10 8
 import { useForm } from '@/composables/useForm'
11 9
 import AppInputText from '@/components/AppInputText.vue'
12 10
 import AppInputNumber from '@/components/AppInputNumber.vue'
@@ -34,6 +32,7 @@ const form = useForm({
34 32
   qty: null,
35 33
   supplier: null,
36 34
   product: null,
35
+  ppn: props.ppn,
37 36
 })
38 37
 
39 38
 const onSubmit = () => {
@@ -41,38 +40,27 @@ const onSubmit = () => {
41 40
     .transform((data) => ({
42 41
       number: data.number,
43 42
       status: data.status,
44
-      price: data.price,
45
-      qty: data.qty,
46 43
       supplier_id: data.supplier.id,
47
-      product_number: data.product.number,
44
+      products: productCart,
48 45
     }))
49 46
     .post(route('purchases.store'), {
50
-      onSuccess: () => form.reset(),
47
+      onSuccess: () => {
48
+        form.reset()
49
+
50
+        onClearProduct()
51
+      },
51 52
     })
52 53
 }
53 54
 
54
-const { cartProduct, onAddProduct, onDeleteProduct, onClearProduct } =
55
-  useProductCart(form)
56
-
57
-const dialog = useDialog()
58
-
59
-const onShowCreateSupplier = () => {
60
-  dialog.open(SupplierCreate, {
61
-    props: {
62
-      header: 'Tambah Supplier',
63
-      ...dialogStyle,
64
-    },
65
-  })
66
-}
55
+const {
56
+  productCart,
57
+  onAddProduct,
58
+  onDeleteProduct,
59
+  onClearProduct,
60
+  totalProductPrice,
61
+} = useProductCart(form)
67 62
 
68
-const onShowCreateProduct = () => {
69
-  dialog.open(ProductCreate, {
70
-    props: {
71
-      header: 'Tambah Produk',
72
-      ...dialogStyle,
73
-    },
74
-  })
75
-}
63
+const { onShowCreateProduct, onShowCreateSupplier } = onShowDialog()
76 64
 </script>
77 65
 
78 66
 <template>
@@ -80,156 +68,179 @@ const onShowCreateProduct = () => {
80 68
     <DynamicDialog />
81 69
 
82 70
     <div class="grid">
83
-      <div class="col-12 md:col-8 flex-order-1 md:flex-order-0">
84
-        <Card>
85
-          <template #title> Pembeli </template>
86
-          <template #content>
87
-            <div class="grid">
88
-              <div class="col-12 md:col-6">
89
-                <AppInputText
90
-                  disabled
91
-                  label="Nomor Pembelian"
92
-                  placeholder="nomor pembelian"
93
-                  :error="form.errors.number"
94
-                  v-model="form.number"
95
-                />
96
-              </div>
97
-
98
-              <div class="col-12 md:col-6">
99
-                <AppDropdown
100
-                  label="Status"
101
-                  placeholder="status"
102
-                  :options="optionStatus"
103
-                  :error="form.errors.status"
104
-                  v-model="form.status"
105
-                />
106
-              </div>
107
-
108
-              <div class="col-12 md:col-6">
109
-                <AppAutoComplete
110
-                  label="Supplier"
111
-                  placeholder="supplier"
112
-                  field="name"
113
-                  refresh-data="suppliers"
114
-                  v-model="form.supplier"
115
-                  :error="form.errors.suppliers_id"
116
-                  :suggestions="suppliers"
117
-                >
118
-                  <template #item="slotProps">
119
-                    <template v-if="slotProps.item">
120
-                      <div class="flex flex-column">
121
-                        <span>{{ slotProps.item.name }}</span>
122
-                        <span>{{ slotProps.item.npwp }}</span>
123
-                      </div>
124
-                    </template>
125
-                  </template>
126
-
127
-                  <template #empty>
128
-                    <span
129
-                      class="cursor-pointer"
130
-                      style="color: var(--primary-color)"
131
-                      @click="onShowCreateSupplier"
71
+      <div class="col-12 md:col-8">
72
+        <div class="grid">
73
+          <div class="col-12">
74
+            <Card>
75
+              <template #title> Pembeli </template>
76
+              <template #content>
77
+                <div class="grid">
78
+                  <div class="col-12 md:col-6">
79
+                    <AppInputText
80
+                      disabled
81
+                      label="Nomor Pembelian"
82
+                      placeholder="nomor pembelian"
83
+                      :error="form.errors.number"
84
+                      v-model="form.number"
85
+                    />
86
+                  </div>
87
+
88
+                  <div class="col-12 md:col-6">
89
+                    <AppDropdown
90
+                      label="Status"
91
+                      placeholder="status"
92
+                      :options="optionStatus"
93
+                      :error="form.errors.status"
94
+                      v-model="form.status"
95
+                    />
96
+                  </div>
97
+
98
+                  <div class="col-12 md:col-6">
99
+                    <AppAutoComplete
100
+                      empty
101
+                      label="Supplier"
102
+                      placeholder="supplier"
103
+                      field="name"
104
+                      refresh-data="suppliers"
105
+                      v-model="form.supplier"
106
+                      :error="form.errors.suppliers_id"
107
+                      :suggestions="suppliers"
108
+                    >
109
+                      <template #item="slotProps">
110
+                        <template v-if="slotProps.item">
111
+                          <div class="flex flex-column">
112
+                            <span>{{ slotProps.item.name }}</span>
113
+                            <span>{{ slotProps.item.npwp }}</span>
114
+                          </div>
115
+                        </template>
116
+                      </template>
117
+
118
+                      <template #empty>
119
+                        <span
120
+                          class="cursor-pointer"
121
+                          style="color: var(--primary-color)"
122
+                          @click="onShowCreateSupplier"
123
+                        >
124
+                          Tambah Supplier
125
+                        </span>
126
+                      </template>
127
+                    </AppAutoComplete>
128
+                  </div>
129
+                </div>
130
+              </template>
131
+            </Card>
132
+          </div>
133
+
134
+          <div class="col-12">
135
+            <Card>
136
+              <template #title>Produk</template>
137
+              <template #content>
138
+                <div class="grid">
139
+                  <div class="col-12 md:col-6">
140
+                    <AppAutoComplete
141
+                      :disabled="!form.supplier?.id"
142
+                      empty
143
+                      label="Produk"
144
+                      placeholder="produk"
145
+                      field="name"
146
+                      refresh-data="products"
147
+                      v-model="form.product"
148
+                      :error="form.errors.product"
149
+                      :suggestions="products"
132 150
                     >
133
-                      Tambah Supplier
134
-                    </span>
135
-                  </template>
136
-                </AppAutoComplete>
137
-              </div>
138
-            </div>
139
-          </template>
140
-        </Card>
151
+                      <template #item="slotProps">
152
+                        <template v-if="slotProps.item">
153
+                          <div class="flex flex-column">
154
+                            <span>{{ slotProps.item.number }}</span>
155
+                            <span>{{ slotProps.item.name }}</span>
156
+                          </div>
157
+                        </template>
158
+                      </template>
159
+
160
+                      <template #empty>
161
+                        <span
162
+                          class="cursor-pointer"
163
+                          style="color: var(--primary-color)"
164
+                          @click="onShowCreateProduct"
165
+                        >
166
+                          Tambah Produk
167
+                        </span>
168
+                      </template>
169
+                    </AppAutoComplete>
170
+                  </div>
171
+
172
+                  <div v-if="form.product?.unit" class="col-12 md:col-6">
173
+                    <AppInputText
174
+                      disabled
175
+                      label="Satuan"
176
+                      placeholder="satuan"
177
+                      v-model="form.product.unit"
178
+                    />
179
+                  </div>
180
+
181
+                  <div class="col-12 md:col-6">
182
+                    <AppInputNumber
183
+                      :disabled="!form.supplier?.id"
184
+                      label="Harga"
185
+                      placeholder="harga"
186
+                      v-model="form.price"
187
+                    />
188
+                  </div>
189
+
190
+                  <div class="col-12 md:col-6">
191
+                    <AppInputText
192
+                      :disabled="!form.supplier?.id"
193
+                      label="Kuantitas"
194
+                      placeholder="kuantitas"
195
+                      type="number"
196
+                      v-model="form.qty"
197
+                    />
198
+                  </div>
199
+                </div>
200
+              </template>
201
+              <template #footer>
202
+                <div class="flex flex-column md:flex-row justify-content-end">
203
+                  <Button
204
+                    label="Tambah Produk"
205
+                    icon="pi pi-check"
206
+                    class="p-button-outlined"
207
+                    :disabled="
208
+                      !form.price || !form.qty || !form.product?.number
209
+                    "
210
+                    @click="onAddProduct"
211
+                  />
212
+                </div>
213
+              </template>
214
+            </Card>
215
+          </div>
216
+
217
+          <div class="col-12">
218
+            <Cart
219
+              title="Keranjang Produk"
220
+              :header-table="cartTable"
221
+              :value="productCart"
222
+              @delete="onDeleteProduct"
223
+            />
224
+          </div>
225
+        </div>
141 226
       </div>
142 227
 
143
-      <div class="col-12 md:col-4 flex-order-4 md:flex-order-0">
228
+      <div class="col-12 md:col-4">
144 229
         <Details
145 230
           title="Detail Pembelian"
231
+          message="Pastikan semua produk sudah benar"
146 232
           :number="number"
147
-          :ppn="ppn"
148 233
           :status="form.status"
149 234
           :person="form.supplier"
150 235
           :product="form.product"
151
-          :disabled="form.processing"
152
-          @click="onSubmit"
153
-        />
154
-      </div>
155
-
156
-      <div class="col-12 md:col-8 flex-order-2 md:flex-order-0">
157
-        <Card>
158
-          <template #title>Produk</template>
159
-          <template #content>
160
-            <div class="grid">
161
-              <div class="col-12 md:col-6">
162
-                <AppAutoComplete
163
-                  label="Produk"
164
-                  placeholder="produk"
165
-                  field="name"
166
-                  refresh-data="products"
167
-                  v-model="form.product"
168
-                  :error="form.errors.product_number"
169
-                  :suggestions="products"
170
-                >
171
-                  <template #item="slotProps">
172
-                    <template v-if="slotProps.item">
173
-                      <div class="flex flex-column">
174
-                        <span>{{ slotProps.item.number }}</span>
175
-                        <span>{{ slotProps.item.name }}</span>
176
-                      </div>
177
-                    </template>
178
-                  </template>
179
-
180
-                  <template #empty>
181
-                    <span
182
-                      class="cursor-pointer"
183
-                      style="color: var(--primary-color)"
184
-                      @click="onShowCreateProduct"
185
-                    >
186
-                      Tambah Produk
187
-                    </span>
188
-                  </template>
189
-                </AppAutoComplete>
190
-              </div>
191
-
192
-              <div class="col-12 md:col-6">
193
-                <AppInputNumber
194
-                  label="Harga"
195
-                  placeholder="harga"
196
-                  :error="form.errors.price"
197
-                  v-model="form.price"
198
-                />
199
-              </div>
200
-
201
-              <div class="col-12 md:col-6">
202
-                <AppInputText
203
-                  label="Kuantitas"
204
-                  placeholder="kuantitas"
205
-                  type="number"
206
-                  :error="form.errors.qty"
207
-                  v-model="form.qty"
208
-                />
209
-              </div>
210
-            </div>
211
-          </template>
212
-          <template #footer>
213
-            <div class="flex flex-column md:flex-row justify-content-end">
214
-              <Button
215
-                label="Tambah Produk"
216
-                icon="pi pi-check"
217
-                class="p-button-outlined"
218
-                :disabled="form.processing"
219
-                @click="onAddProduct"
220
-              />
221
-            </div>
222
-          </template>
223
-        </Card>
224
-      </div>
225
-
226
-      <div class="col-12 md:col-8 flex-order-3 md:flex-order-0">
227
-        <Cart
228
-          title="Keranjang Produk"
229
-          :header-table="cartTable"
230
-          :form="form"
231
-          :value="cartProduct"
232
-          @delete="onDeleteProduct"
236
+          :price="totalProductPrice()"
237
+          :disabled="
238
+            form.processing ||
239
+            !form.status ||
240
+            !form.supplier?.id ||
241
+            productCart.length === 0
242
+          "
243
+          @submit="onSubmit"
233 244
         />
234 245
       </div>
235 246
     </div>

+ 6
- 8
resources/js/pages/Purchases/Index.vue Visa fil

@@ -25,14 +25,12 @@ defineProps({
25 25
 
26 26
         <div class="grid">
27 27
           <div class="col-12 md:col-8">
28
-            <div class="flex align-items-center">
29
-              <AppSearch
30
-                class="w-full md:w-27rem"
31
-                placeholder="cari, contoh: PBNxx, Pending, Success"
32
-                url="/purchases"
33
-                :initial-search="initialSearch"
34
-              />
35
-            </div>
28
+            <AppSearch
29
+              class="w-full md:w-27rem"
30
+              placeholder="nama, email, phone, status"
31
+              url="/purchases"
32
+              :initial-search="initialSearch"
33
+            />
36 34
           </div>
37 35
 
38 36
           <div

+ 7
- 18
resources/js/pages/Purchases/config.js Visa fil

@@ -11,29 +11,18 @@ export const optionStatus = [
11 11
 
12 12
 export const indexTable = [
13 13
   { field: 'updatedAt', header: 'Tanggal' },
14
-  { field: 'number', header: 'Nomor Penjualan' },
14
+  { field: 'name', header: 'Nama Supplier' },
15
+  { field: 'phone', header: 'No HP Supplier' },
16
+  { field: 'email', header: 'Email Supplier' },
17
+  { field: 'price', header: 'Total Harga' },
15 18
   { field: 'status', header: 'Status' },
16
-  { field: 'price', header: 'Harga' },
17
-  { field: 'qty', header: 'Kuantitas' },
18
-  { field: 'ppn', header: 'PPN' },
19
-  { field: 'productName', header: 'Nama Produk' },
20
-  { field: 'productNumber', header: 'Nomor Produk' },
21 19
 ]
22 20
 
23 21
 export const cartTable = [
24 22
   { field: 'number', header: 'Nomor Produk' },
25 23
   { field: 'name', header: 'Produk' },
24
+  { field: 'ppn', header: 'PPN' },
26 25
   { field: 'price', header: 'Harga' },
27
-  { field: 'quantity', header: 'Kuantitas' },
26
+  { field: 'qty', header: 'Kuantitas' },
27
+  { field: 'unit', header: 'Satuan' },
28 28
 ]
29
-
30
-export const dialogStyle = {
31
-  style: {
32
-    width: '50vw',
33
-  },
34
-  breakpoints: {
35
-    '960px': '75vw',
36
-    '640px': '90vw',
37
-  },
38
-  modal: true,
39
-}