diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 3834aa9..3391bd8 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -405,7 +405,8 @@ public function store(Request $request) 'title' => 'nullable|string', 'text' => 'nullable|string', 'items' => 'nullable|array', - 'items.*.position' => 'required|integer', + 'items.*.isSection' => 'nullable|boolean', + 'items.*.position' => 'required|numeric', 'items.*.title' => 'nullable|string', 'items.*.description' => 'nullable|string', 'items.*.quantity' => 'nullable|numeric', @@ -491,7 +492,8 @@ public function update(Request $request, $id) 'text' => 'nullable|string', 'items' => 'nullable|array', 'items.*.id' => 'nullable|integer', - 'items.*.position' => 'required|integer', + 'items.*.isSection' => 'nullable|boolean', + 'items.*.position' => 'required|numeric', 'items.*.title' => 'nullable|string', 'items.*.description' => 'nullable|string', 'items.*.quantity' => 'nullable|numeric', diff --git a/app/Models/LineItem.php b/app/Models/LineItem.php index 0fa9ad6..d07fbe5 100644 --- a/app/Models/LineItem.php +++ b/app/Models/LineItem.php @@ -12,6 +12,7 @@ class LineItem extends Model protected $fillable = [ 'invoice_id', 'position', + 'is_section', 'title', 'description', 'quantity', diff --git a/database/factories/LineItemFactory.php b/database/factories/LineItemFactory.php index 7efbef0..911ee36 100644 --- a/database/factories/LineItemFactory.php +++ b/database/factories/LineItemFactory.php @@ -8,8 +8,11 @@ class LineItemFactory extends Factory { public function definition(): array { + + $isSection = rand(0, 10) > 7; return [ - 'position' => $this->faker->numberBetween(1, 10), + 'position' => $this->faker->numberBetween(1, 10) + ($isSection ? 0.5 : 0), + 'is_section' => $isSection, 'title' => $this->faker->words(3, true), 'description' => $this->faker->sentence(), 'quantity' => $this->faker->numberBetween(1, 10), @@ -17,4 +20,4 @@ public function definition(): array 'price' => $this->faker->randomFloat(2, 10, 500), ]; } -} \ No newline at end of file +} diff --git a/database/migrations/2025_09_18_193251_create_line_items_table.php b/database/migrations/2025_09_18_193251_create_line_items_table.php index 3846e79..c6f3e5b 100644 --- a/database/migrations/2025_09_18_193251_create_line_items_table.php +++ b/database/migrations/2025_09_18_193251_create_line_items_table.php @@ -14,7 +14,8 @@ public function up(): void Schema::create('line_items', function (Blueprint $table) { $table->id(); $table->foreignId('invoice_id')->constrained()->onDelete('cascade'); - $table->integer('position')->default(0); + $table->decimal('position', 10, 2)->default(0); + $table->boolean('is_section')->default(false); $table->string('title')->nullable(); $table->string('description')->nullable(); $table->integer('quantity')->default(1); diff --git a/resources/js/components/documents/InvoiceDialog.vue b/resources/js/components/documents/InvoiceDialog.vue index 03dafd0..16180f6 100644 --- a/resources/js/components/documents/InvoiceDialog.vue +++ b/resources/js/components/documents/InvoiceDialog.vue @@ -296,6 +296,7 @@ const save = async () => { items: invoice.value.items.map(item => ({ id: item.id, // Include ID for existing items position: item.position, + isSection: item.isSection, type: item.type, title: item.title, description: item.description, @@ -323,6 +324,7 @@ const save = async () => { } finally { // remove spinner from save button isSaving.value = false + setTimeout(() => { isDirty.value = false }, 1000) } } diff --git a/resources/js/components/documents/LineItemTable.vue b/resources/js/components/documents/LineItemTable.vue index d46e4de..add4adb 100644 --- a/resources/js/components/documents/LineItemTable.vue +++ b/resources/js/components/documents/LineItemTable.vue @@ -47,13 +47,23 @@ watch(() => props.lineItems, (newLineItems) => { items.value = (newLineItems ?? []).slice().sort(function (a, b) { return a.position - b.position }) }, { deep: true, once: true }) -const newItem = () => { - const position = items.value.length + 1 - let item = newLineItem() - item.position = position +const newItem = (isSection: boolean) => { + let item = newLineItem(isSection) + item.position = getNextPosition(items.value.length, isSection) items.value.push(item) } +const getNextPosition = (index: number, isSection: boolean) => { + let lastPosition = (index == 0) ? 0 : items.value.at(index - 1)?.position ?? 0 + let nextInteger = (lastPosition == Math.ceil(lastPosition)) ? lastPosition + 1 : Math.ceil(lastPosition) + + // item: next integer + // sections: middle between last element and next integer + return isSection ? + lastPosition + (nextInteger - lastPosition) / 2 + : nextInteger +} + const deleteItem = (lineItem: LineItem) => { const index = items.value.indexOf(lineItem) if (index > -1) { @@ -64,7 +74,7 @@ const deleteItem = (lineItem: LineItem) => { const recalculatePositions = () => { for (let i = 0; i < items.value.length; i++) { - items.value[i].position = i + 1 + items.value[i].position = getNextPosition(i, items.value[i].isSection) } } @@ -99,9 +109,32 @@ const recalculatePositions = () => { @end="recalculatePositions">