chore: update document approval workflow and bug fixes

This commit is contained in:
2026-05-25 13:05:20 +08:00
parent d573c02893
commit 39a8e1d4cd
910 changed files with 49994 additions and 1010 deletions

View File

@@ -0,0 +1,98 @@
<?php
namespace Modules\MasterData\Exports;
use Maatwebsite\Excel\Concerns\FromArray;
use Maatwebsite\Excel\Concerns\WithStyles;
use Maatwebsite\Excel\Concerns\WithColumnWidths;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class BoqTemplateExport implements FromArray, WithStyles, WithColumnWidths
{
public function array(): array
{
return [
// Header section (A=label, B=value)
['PROJECT NAME', ''],
['OWNER NAME', ''],
['LOCATION', ''],
['DATE', ''],
[],
// Detail table header
['BILL OF QUANTITIES'],
['Item no.', 'Description of Materials', 'UNIT', 'APPROVED QUANTITY', 'REQUEST QUANTITY', 'SYSTEM /WORK AREA'],
// Sample rows
['1', '200mm thk aggregate sub-base course', 'cu.m', '2970.00', '505.00', 'Access Road'],
['2', '300mm thk portland cement concrete pavement', 'cu.m', '4240.00', '721.00', 'Access Road'],
['3', '6mm thk bituminous joint sealant', 'lm', '1460.00', '90', 'Access Road'],
];
}
public function columnWidths(): array
{
return [
'A' => 12,
'B' => 50,
'C' => 12,
'D' => 20,
'E' => 20,
'F' => 25,
];
}
public function styles(Worksheet $sheet): array
{
// Merge header label cells for the title section
$sheet->mergeCells('B1:F1');
$sheet->mergeCells('B2:F2');
$sheet->mergeCells('B3:F3');
$sheet->mergeCells('B4:F4');
// BOQ title row
$sheet->mergeCells('A6:F6');
$sheet->getStyle('A6')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
return [
// Header labels (A1:A4)
'A1:A4' => [
'font' => ['bold' => true, 'size' => 11],
'fill' => [
'fillType' => Fill::FILL_SOLID,
'startColor' => ['argb' => 'FFD9E1F2'],
],
],
// BOQ title row
6 => [
'font' => ['bold' => true, 'size' => 14, 'color' => ['argb' => 'FFFFFFFF']],
'fill' => [
'fillType' => Fill::FILL_SOLID,
'startColor' => ['argb' => 'FF2F5496'],
],
],
// Detail table header (row 7)
7 => [
'font' => ['bold' => true, 'size' => 10],
'fill' => [
'fillType' => Fill::FILL_SOLID,
'startColor' => ['argb' => 'FFFFFF00'],
],
'borders' => [
'allBorders' => ['borderStyle' => Border::BORDER_THIN],
],
],
// Sample data rows
'8:10' => [
'fill' => [
'fillType' => Fill::FILL_SOLID,
'startColor' => ['argb' => 'FFFFFFCC'],
],
'borders' => [
'allBorders' => ['borderStyle' => Border::BORDER_THIN],
],
],
];
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Modules\MasterData\Exports;
use Maatwebsite\Excel\Concerns\FromArray;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithStyles;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class MaterialTemplateExport implements FromArray, WithHeadings, WithStyles
{
public function headings(): array
{
return ['Name', 'SKU', 'Category', 'Unit', 'Unit Cost', 'Description', 'Group'];
}
public function array(): array
{
return [
['Portland Cement Type I', 'CEM-001', 'cement', 'bag', '280.00', '50kg bag', 'Foundation Materials'],
['Deformed Steel Bar 16mm', 'STL-016', 'steel', 'pcs', '450.00', '6m length', 'Structural Materials'],
];
}
public function styles(Worksheet $sheet): array
{
return [
1 => [
'font' => ['bold' => true],
'fill' => [
'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID,
'startColor' => ['argb' => 'FFE8E8E8'],
],
],
];
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Modules\MasterData\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class MasterDataController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
return view('masterdata::index');
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
return view('masterdata::create');
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request) {}
/**
* Show the specified resource.
*/
public function show($id)
{
return view('masterdata::show');
}
/**
* Show the form for editing the specified resource.
*/
public function edit($id)
{
return view('masterdata::edit');
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, $id) {}
/**
* Remove the specified resource from storage.
*/
public function destroy($id) {}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Modules\MasterData\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Modules\MasterData\Models\Material;
use Modules\MasterData\Models\MaterialGroup;
class MaterialGroupController extends Controller
{
public function index(Request $request)
{
$query = MaterialGroup::withCount('materials');
if ($search = $request->search) {
$query->where('name', 'like', "%{$search}%");
}
if ($status = $request->status) {
$query->where('status', $status);
}
$groups = $query->latest()->paginate(15)->withQueryString();
$allMaterials = Material::where('status', 'active')
->select('id', 'ulid', 'name', 'sku', 'unit', 'unit_cost', 'category')
->get();
return Inertia::render('MaterialLogistics::Groups/Index', [
'groups' => $groups,
'allMaterials' => $allMaterials,
'filters' => $request->only(['search', 'status']),
]);
}
public function show(MaterialGroup $materialGroup)
{
$materialGroup->load('materials:id,ulid,name,sku,unit,unit_cost,category');
return response()->json($materialGroup);
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'description' => 'nullable|string',
]);
$group = MaterialGroup::create($validated);
if ($request->has('material_ids') && is_array($request->material_ids)) {
$materialIds = Material::whereIn('ulid', $request->material_ids)->pluck('id');
$group->materials()->attach($materialIds);
}
return back()->with('success', "Material group \"{$group->name}\" created.");
}
public function update(Request $request, MaterialGroup $materialGroup)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'status' => 'nullable|in:active,archived',
]);
$materialGroup->update($validated);
return back()->with('success', 'Group updated.');
}
public function destroy(MaterialGroup $materialGroup)
{
if ($materialGroup->projects()->exists()) {
return back()->with('error', 'Cannot delete a group that is assigned to projects. Remove it from all projects first.');
}
$name = $materialGroup->name;
$materialGroup->delete();
return back()->with('success', "Group \"{$name}\" deleted.");
}
public function addMaterial(Request $request, MaterialGroup $materialGroup)
{
$request->validate([
'material_id' => 'required|string',
]);
$material = Material::where('ulid', $request->material_id)->firstOrFail();
if ($materialGroup->materials()->where('material_id', $material->id)->exists()) {
return back()->with('error', 'Material is already in this group.');
}
$materialGroup->materials()->attach($material->id);
return back()->with('success', "{$material->name} added to group.");
}
public function removeMaterial(MaterialGroup $materialGroup, Material $material)
{
$materialGroup->materials()->detach($material->id);
return back()->with('success', "{$material->name} removed from group.");
}
}

View File

@@ -0,0 +1,167 @@
<?php
namespace Modules\MasterData\Http\Controllers;
use App\Http\Controllers\Controller;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Facades\Excel;
use Modules\MasterData\Exports\BoqTemplateExport;
use Modules\MasterData\Exports\MaterialTemplateExport;
use Modules\MasterData\Imports\BoqImport;
use Modules\MasterData\Imports\MaterialImport;
use Modules\MasterData\Models\Material;
use Modules\ProjectManagement\Models\Project;
use Modules\ProjectManagement\Models\Task;
class MaterialImportController extends Controller
{
public function template()
{
return Excel::download(new MaterialTemplateExport(), 'material_import_template.xlsx');
}
public function import(Request $request)
{
$request->validate([
'file' => 'required|file|mimes:xlsx,csv,xls|max:10240',
]);
$import = new MaterialImport();
try {
Excel::import($import, $request->file('file'));
} catch (\Exception $e) {
return back()->with('error', 'Import failed: ' . $e->getMessage());
}
$failures = $import->failures();
$failureCount = $failures->count();
$message = "{$import->getImportedCount()} materials imported.";
if ($import->getSkippedCount() > 0) {
$message .= " {$import->getSkippedCount()} skipped (duplicate SKU).";
}
if (count($import->getGroupsCreated()) > 0) {
$message .= " Groups: " . implode(', ', $import->getGroupsCreated()) . ".";
}
if ($failureCount > 0) {
$message .= " {$failureCount} rows had validation errors.";
}
return back()->with('success', $message);
}
// --- BOQ Import ---
public function boqTemplate()
{
return Excel::download(new BoqTemplateExport(), 'boq_template.xlsx');
}
public function boqPreview(Request $request)
{
$request->validate([
'file' => 'required|file|mimes:xlsx,csv,xls|max:10240',
]);
$import = new BoqImport();
try {
Excel::import($import, $request->file('file'));
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to parse file: ' . $e->getMessage(),
], 422);
}
if (!empty($import->getErrors())) {
return response()->json([
'success' => false,
'message' => implode('; ', $import->getErrors()),
], 422);
}
return response()->json([
'success' => true,
'header' => $import->getHeader(),
'items' => $import->getItems(),
]);
}
public function boqConfirm(Request $request)
{
$validated = $request->validate([
'project_ulid' => 'required|string',
'task_ulid' => 'required|string',
'header' => 'required|array',
'header.project_name' => 'nullable|string',
'header.owner_name' => 'nullable|string',
'header.location' => 'nullable|string',
'header.date' => 'nullable|string',
'items' => 'required|array|min:1',
'items.*.material' => 'required|string|max:255',
'items.*.unit' => 'required|string|max:20',
'items.*.approved_qty' => 'nullable|numeric|min:0',
'items.*.request_qty' => 'nullable|numeric|min:0',
'items.*.work_area' => 'nullable|string|max:255',
]);
$project = Project::where('ulid', $validated['project_ulid'])->firstOrFail();
$task = Task::where('ulid', $validated['task_ulid'])
->where('project_id', $project->id)
->firstOrFail();
$created = 0;
DB::transaction(function () use ($validated, $project, $task, &$created) {
// Update project fields from header
$updates = [];
if (!empty($validated['header']['location'])) {
$updates['location'] = $validated['header']['location'];
}
if (!empty($validated['header']['date'])) {
try {
$updates['start_date'] = Carbon::parse($validated['header']['date'])->format('Y-m-d');
} catch (\Exception) {
// Skip invalid date
}
}
if (!empty($updates)) {
$project->update($updates);
}
// Create materials and assign to task
foreach ($validated['items'] as $item) {
$material = Material::create([
'name' => $item['material'],
'unit' => $item['unit'],
'unit_cost' => 0,
'category' => null,
'status' => 'active',
]);
$task->taskMaterials()->create([
'material_id' => $material->id,
'planned_qty' => $item['request_qty'] ?? 0,
'actual_qty' => $item['approved_qty'] ?? 0,
'unit_cost' => 0,
'notes' => $item['work_area'] ?? null,
]);
$created++;
}
// Recalculate project capitalization
$project->load('tasks.taskMaterials');
$project->recalculateCapitalization();
});
return back()->with('success', "{$created} materials imported and assigned to task \"{$task->name}\".");
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace Modules\MasterData\Imports;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\ToCollection;
class BoqImport implements ToCollection
{
private array $header = [];
private array $items = [];
private array $errors = [];
/**
* Real file layout:
* Rows 0-3: Header (A=label, B=value) PROJECT NAME, OWNER NAME, LOCATION, DATE
* Row 4: Empty
* Row 5: "BILL OF QUANTITIES" title
* Row 6: Column headers (Item no. | Description of Materials | UNIT | APPROVED QUANTITY | REQUEST QUANTITY | SYSTEM /WORK AREA)
* Row 7+: Data rows
*
* Mapping:
* B = material name (filter out "Description of Materials")
* C = unit
* D = APPROVED QUANTITY actual_qty
* E = REQUEST QUANTITY planned_qty
* F = SYSTEM /WORK AREA notes
*/
public function collection(Collection $rows): void
{
// Parse header section (rows 0-3 → A=label, B=value)
foreach ($rows->slice(0, 4) as $row) {
$label = strtolower(trim($row[0] ?? ''));
$value = trim($row[1] ?? '');
match (true) {
str_contains($label, 'project') => $this->header['project_name'] = $value,
str_contains($label, 'owner') => $this->header['owner_name'] = $value,
str_contains($label, 'location') => $this->header['location'] = $value,
str_contains($label, 'date') => $this->header['date'] = $value,
default => null,
};
}
// Find the detail table header row by scanning for "Description of Materials" in column B
$dataStartIndex = null;
foreach ($rows as $index => $row) {
$colB = strtolower(trim($row[1] ?? ''));
if (str_contains($colB, 'description') && str_contains($colB, 'material')) {
$dataStartIndex = $index + 1;
break;
}
}
// Fallback: look for "Item" in column A
if ($dataStartIndex === null) {
foreach ($rows as $index => $row) {
$colA = strtolower(trim($row[0] ?? ''));
if (str_contains($colA, 'item') && str_contains($colA, 'no')) {
$dataStartIndex = $index + 1;
break;
}
}
}
if ($dataStartIndex === null) {
$this->errors[] = 'Could not find the detail table header row. Expected "Description of Materials" in column B.';
return;
}
// Parse detail rows
$itemNumber = 0;
foreach ($rows->slice($dataStartIndex) as $row) {
$materialName = trim($row[1] ?? '');
// Skip empty rows and the header text itself
if (empty($materialName)) {
continue;
}
// Filter out rows that contain the header text
if (str_contains(strtolower($materialName), 'description of material')) {
continue;
}
$itemNumber++;
$unit = strtolower(trim($row[2] ?? 'pcs'));
$approvedQty = $this->parseNumber($row[3] ?? 0);
$requestQty = $this->parseNumber($row[4] ?? 0);
$workArea = trim($row[5] ?? '');
$errors = [];
if ($requestQty <= 0 && $approvedQty <= 0) {
$errors[] = 'No quantity specified';
}
$this->items[] = [
'item_no' => $row[0] ?? $itemNumber,
'material' => $materialName,
'unit' => $unit,
'approved_qty' => $approvedQty,
'request_qty' => $requestQty,
'work_area' => $workArea,
'errors' => $errors,
];
}
}
private function parseNumber(mixed $value): float
{
if (is_numeric($value)) {
return (float) $value;
}
// Handle comma-formatted numbers like "2,970.00"
$cleaned = str_replace(',', '', (string) $value);
return is_numeric($cleaned) ? (float) $cleaned : 0;
}
public function getHeader(): array
{
return $this->header;
}
public function getItems(): array
{
return $this->items;
}
public function getErrors(): array
{
return $this->errors;
}
public function hasErrors(): bool
{
if (!empty($this->errors)) return true;
foreach ($this->items as $item) {
if (!empty($item['errors'])) return true;
}
return false;
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Modules\MasterData\Imports;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Maatwebsite\Excel\Concerns\WithValidation;
use Maatwebsite\Excel\Concerns\SkipsOnFailure;
use Maatwebsite\Excel\Concerns\SkipsFailures;
use Modules\MasterData\Models\Material;
use Modules\MasterData\Models\MaterialGroup;
class MaterialImport implements ToCollection, WithHeadingRow, WithValidation, SkipsOnFailure
{
use SkipsFailures;
private int $importedCount = 0;
private int $skippedCount = 0;
private array $groupsCreated = [];
public function collection(Collection $rows): void
{
foreach ($rows as $row) {
// Skip completely empty rows
if (empty($row['name'])) {
continue;
}
$sku = $row['sku'] ?? null;
$name = $row['name'];
$matchAttributes = [];
if (!empty($sku)) {
$matchAttributes['sku'] = $sku;
} else {
$matchAttributes['name'] = $name;
}
$material = Material::updateOrCreate(
$matchAttributes,
[
'name' => $name,
'sku' => $sku,
'category' => $row['category'] ?? null,
'unit' => $row['unit'] ?? 'pcs',
'unit_cost' => $row['unit_cost'] ?? 0,
'description' => $row['description'] ?? null,
]
);
// Handle group assignment
$groupName = trim($row['group'] ?? '');
if ($groupName) {
$group = MaterialGroup::firstOrCreate(
['name' => $groupName],
['description' => "Auto-created from import"]
);
$group->materials()->syncWithoutDetaching([$material->id]);
if (!in_array($groupName, $this->groupsCreated)) {
$this->groupsCreated[] = $groupName;
}
}
$this->importedCount++;
}
}
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'sku' => 'nullable|string|max:50',
'category' => 'nullable|string|max:50',
'unit' => 'nullable|string|max:20',
'unit_cost' => 'nullable|numeric|min:0',
'description' => 'nullable|string',
'group' => 'nullable|string|max:255',
];
}
public function getImportedCount(): int
{
return $this->importedCount;
}
public function getSkippedCount(): int
{
return $this->skippedCount;
}
public function getGroupsCreated(): array
{
return $this->groupsCreated;
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Modules\MasterData\Models;
use App\Traits\BelongsToTenant;
use App\Traits\HasPublicIdentifier;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Modules\MaterialLogistics\Models\WarehouseStock;
use Modules\MaterialLogistics\Models\ProjectInventory;
use Modules\MaterialLogistics\Models\MaterialRequirement;
use Modules\MaterialLogistics\Models\MaterialDeployment;
class Material extends Model
{
use HasPublicIdentifier, BelongsToTenant;
protected $fillable = [
'name', 'sku', 'category', 'unit',
'unit_cost', 'description', 'status', 'type', 'contractor_id',
];
protected function casts(): array
{
return [
'unit_cost' => 'decimal:2',
];
}
public function components(): HasMany
{
return $this->hasMany(MaterialComponent::class, 'parent_id');
}
public function parentKits(): HasMany
{
return $this->hasMany(MaterialComponent::class, 'component_id');
}
public function warehouseStocks(): HasMany
{
return $this->hasMany(WarehouseStock::class);
}
public function projectInventories(): HasMany
{
return $this->hasMany(ProjectInventory::class);
}
public function inventories(): HasMany
{
return $this->hasMany(ProjectInventory::class);
}
public function requirements(): HasMany
{
return $this->hasMany(MaterialRequirement::class);
}
public function deployments(): HasMany
{
return $this->hasMany(MaterialDeployment::class);
}
public function groups(): BelongsToMany
{
return $this->belongsToMany(MaterialGroup::class, 'material_group_items')
->withTimestamps();
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Modules\MasterData\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class MaterialComponent extends Model
{
protected $table = 'material_components';
protected $fillable = [
'parent_id',
'component_id',
'quantity',
];
public function parent(): BelongsTo
{
return $this->belongsTo(Material::class, 'parent_id');
}
public function component(): BelongsTo
{
return $this->belongsTo(Material::class, 'component_id');
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Modules\MasterData\Models;
use App\Traits\HasPublicIdentifier;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Modules\ProjectManagement\Models\Project;
class MaterialGroup extends Model
{
use HasPublicIdentifier;
protected $fillable = ['name', 'description', 'status'];
public function materials(): BelongsToMany
{
return $this->belongsToMany(Material::class, 'material_group_items')
->withTimestamps();
}
public function projects(): BelongsToMany
{
return $this->belongsToMany(Project::class, 'project_material_groups')
->withTimestamps();
}
public function scopeActive($query)
{
return $query->where('status', 'active');
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Modules\MasterData\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\Pivot;
class MaterialGroupItem extends Pivot
{
protected $table = 'material_group_items';
public $incrementing = true;
public function group(): BelongsTo
{
return $this->belongsTo(MaterialGroup::class, 'material_group_id');
}
public function material(): BelongsTo
{
return $this->belongsTo(Material::class);
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Modules\MasterData\Providers;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* The event handler mappings for the application.
*
* @var array<string, array<int, string>>
*/
protected $listen = [];
/**
* Indicates if events should be discovered.
*
* @var bool
*/
protected static $shouldDiscoverEvents = true;
/**
* Configure the proper event listeners for email verification.
*/
protected function configureEmailVerification(): void {}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Modules\MasterData\Providers;
use Nwidart\Modules\Support\ModuleServiceProvider;
use Illuminate\Console\Scheduling\Schedule;
class MasterDataServiceProvider extends ModuleServiceProvider
{
/**
* The name of the module.
*/
protected string $name = 'MasterData';
/**
* The lowercase version of the module name.
*/
protected string $nameLower = 'masterdata';
/**
* Command classes to register.
*
* @var string[]
*/
// protected array $commands = [];
/**
* Provider classes to register.
*
* @var string[]
*/
protected array $providers = [
EventServiceProvider::class,
RouteServiceProvider::class,
];
/**
* Define module schedules.
*
* @param $schedule
*/
// protected function configureSchedules(Schedule $schedule): void
// {
// $schedule->command('inspire')->hourly();
// }
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Modules\MasterData\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
protected string $name = 'MasterData';
/**
* Called before routes are registered.
*
* Register any model bindings or pattern based filters.
*/
public function boot(): void
{
parent::boot();
}
/**
* Define the routes for the application.
*/
public function map(): void
{
$this->mapApiRoutes();
$this->mapWebRoutes();
}
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*/
protected function mapWebRoutes(): void
{
Route::middleware('web')->group(module_path($this->name, '/routes/web.php'));
}
/**
* Define the "api" routes for the application.
*
* These routes are typically stateless.
*/
protected function mapApiRoutes(): void
{
Route::middleware('api')->prefix('api')->name('api.')->group(module_path($this->name, '/routes/api.php'));
}
}

View File

@@ -0,0 +1,30 @@
{
"name": "nwidart/masterdata",
"description": "",
"authors": [
{
"name": "Nicolas Widart",
"email": "n.widart@gmail.com"
}
],
"extra": {
"laravel": {
"providers": [],
"aliases": {
}
}
},
"autoload": {
"psr-4": {
"Modules\\MasterData\\": "app/",
"Modules\\MasterData\\Database\\Factories\\": "database/factories/",
"Modules\\MasterData\\Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Modules\\MasterData\\Tests\\": "tests/"
}
}
}

View File

View File

@@ -0,0 +1,5 @@
<?php
return [
'name' => 'MasterData',
];

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('materials', function (Blueprint $table) {
$table->enum('type', ['single', 'kit', 'assembly'])->default('single')->after('description');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('materials', function (Blueprint $table) {
$table->dropColumn('type');
});
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('material_components', function (Blueprint $table) {
$table->id();
$table->foreignId('parent_id')->constrained('materials')->cascadeOnDelete();
$table->foreignId('component_id')->constrained('materials')->cascadeOnDelete();
$table->decimal('quantity', 12, 4);
$table->timestamps();
$table->unique(['parent_id', 'component_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('material_components');
}
};

View File

@@ -0,0 +1,16 @@
<?php
namespace Modules\MasterData\Database\Seeders;
use Illuminate\Database\Seeder;
class MasterDataDatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// $this->call([]);
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "MasterData",
"alias": "masterdata",
"description": "",
"keywords": [],
"priority": 0,
"providers": [
"Modules\\MasterData\\Providers\\MasterDataServiceProvider"
],
"files": []
}

View File

@@ -0,0 +1,15 @@
{
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"axios": "^1.1.2",
"laravel-vite-plugin": "^0.7.5",
"sass": "^1.69.5",
"postcss": "^8.3.7",
"vite": "^4.0.0"
}
}

View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>MasterData Module - {{ config('app.name', 'Laravel') }}</title>
<meta name="description" content="{{ $description ?? '' }}">
<meta name="keywords" content="{{ $keywords ?? '' }}">
<meta name="author" content="{{ $author ?? '' }}">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
{{-- Vite CSS --}}
{{-- {{ module_vite('build-masterdata', 'resources/assets/sass/app.scss') }} --}}
</head>
<body>
{{ $slot }}
{{-- Vite JS --}}
{{-- {{ module_vite('build-masterdata', 'resources/assets/js/app.js') }} --}}
</body>
</html>

View File

@@ -0,0 +1,5 @@
<x-masterdata::layouts.master>
<h1>Hello World</h1>
<p>Module: {!! config('masterdata.name') !!}</p>
</x-masterdata::layouts.master>

View File

View File

@@ -0,0 +1,8 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\MasterData\Http\Controllers\MasterDataController;
Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () {
Route::apiResource('masterdatas', MasterDataController::class)->names('masterdata');
});

View File

@@ -0,0 +1,8 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\MasterData\Http\Controllers\MasterDataController;
Route::middleware(['auth', 'verified'])->group(function () {
Route::resource('masterdatas', MasterDataController::class)->names('masterdata');
});

View File

View File

@@ -0,0 +1,41 @@
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
// Uncomment the import for your frontend framework:
// import vue from '@vitejs/plugin-vue';
// import react from '@vitejs/plugin-react';
// import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
build: {
outDir: '../../public/build-masterdata',
emptyOutDir: true,
manifest: true,
},
plugins: [
laravel({
publicDirectory: '../../public',
buildDirectory: 'build-masterdata',
input: [
__dirname + '/resources/assets/sass/app.scss',
__dirname + '/resources/assets/js/app.js'
],
refresh: true,
}),
// Uncomment the plugin for your frontend framework:
// vue({
// template: {
// transformAssetUrls: {
// base: null,
// includeAbsolute: false,
// },
// },
// }),
// react(),
// svelte(),
],
resolve: {
alias: {
'@': __dirname + '/resources/js',
},
},
});