Tema
SummaryCityLeader.md — Resource "Ringkasan Penjualan City Leader"
Dokumen langkah eksekusi (awal → akhir) untuk resource baru di group Pos.
0. Keputusan yang sudah disepakati
| No | Keputusan | Nilai |
|---|---|---|
| 1 | Label navigasi | Ringkasan Penjualan City Leader |
| 2 | Group navigasi | Pos (sama dengan CityLeaderResource, PosCompanyResource, PosUserResource, CityCaptainResource) |
| 3 | Model | App\Models\CityLeader (mysql.city_leaders) — resource kedua atas model yang sama |
| 4 | Cakupan | Read-only: hanya index + view. Tanpa create/edit/delete/bulk delete |
| 5 | Chart | 2 bar chart terpisah: (a) Penjualan Dropshipper, (b) Penjualan Motorist |
| 6 | Dataset | Per penjual — 1 dataset per dropshipper / per motorist (pola Dashboard) |
| 7 | Sumber data | Dropshipper → pos_invoices; Motorist → motorist_invoices (via pos_sales_by_motorist) |
| 8 | Klik bar | Buka popup modal berisi info tanggal/total + tabel invoice + expand daftar produk |
| 9 | Tanggal default | 7 hari terakhir (start_date / end_date), bisa diubah lewat filter widget |
Alur pengguna
Nav "Pos" → Ringkasan Penjualan City Leader
└─ Tabel: Nama City Leader | Jumlah Dropshipper | Jumlah Motorist (klik baris)
└─ Halaman View (1 city leader)
├─ Section "Penjualan Dropshipper" → bar chart ──klik bar──┐
└─ Section "Penjualan Motorist" → bar chart ──klik bar──┤
▼
Modal "Detail Penjualan"
(info tanggal/total + invoice + produk)1. Peta relasi (dibaca sebelum coding)
CityLeader conn: mysql table: city_leaders
├── posCompanies() HAS MANY PosCompany (city_leader_id) conn: mysql_pos [BARU]
│ └── posUsers (company_id)
│ FILTER is_dropshipper = true AND deleted_at IS NULL → JUMLAH DROPSHIPPER
│
└── motoristResellers() HAS MANY Motorist\User (city_leader_id)
FILTER is_reseller = true → JUMLAH MOTORISTRantai kunci:
| Data | Rantai koneksi |
|---|---|
| Dropshipper count / id | city_leaders.id → pos_company.city_leader_id (mysql_pos) → pos_users.company_id (mysql_pos) + is_dropshipper=1 + deleted_at IS NULL |
| Motorist count / id | city_leaders.id → motorist_users.city_leader_id (mysql) + is_reseller=1 |
| Penjualan dropshipper | pos_invoices.user_id ∈ id dropshipper (mysql_pos) |
| Penjualan motorist | pos_sales_by_motorist.motorist_invoice_id → motorist_invoices.id → motorist_invoice_items → motorist_canvas_details.id_motorist ∈ id motorist (mysql) |
| Detail invoice dropshipper | pos_invoices → pos_invoice_items → pos_products (mysql_pos) |
| Detail invoice motorist | motorist_invoices → motorist_invoice_items → motorist_canvas_details → daftar_barang_dan_jasa (mysql) |
Aturan cross-connection: jangan JOIN lintas DB untuk mengambil id — ambil id list dulu dari koneksi asal, lalu whereIn() di koneksi tujuan. Pola eksisting: App\Services\PosUserCityLeaderService::companyIdsForCityLeader().
Join balik
pos_sales_by_motorist(mysql_pos) ke tabelmotorist_*(mysql) memakai nama DB lengkap$erpDb.'.tabel'— pola yang sudah terbukti jalan diPosSalesChartWidget.php:113-116.
Aturan filter barang (wajib sama dengan Dashboard, PosSalesChartWidget.php:88-94): hanya invoice yang punya minimal 1 item dengan pos_products.material_id IS NOT NULL (barang persediaan, bukan jasa).
2. Langkah eksekusi
Langkah 0 — Prasyarat ✅ (sudah diverifikasi)
- Schema DB terverifikasi (via Docker, koneksi remote):
motorist_users→ kolomcity_leader_id,is_resellerada.pos_company→ kolomcity_leader_idada;pos_users→is_dropshipper,is_city_leader,deleted_atada.pos_invoices→invoice_number,date_time,user_id,subtotal,tax,total,deleted_atada.pos_sales_by_motorist→motorist_invoice_id,created_atada;motorist_canvas_details→id_motorist,agent_idada.- Akses cross-DB dari koneksi
mysql_poske tabelmotorist_*berfungsi.
- Environment:
php artisanboot normal,docker composejalan, koneksimysql+mysql_posOK. - Catatan gaya: ikuti
AGENTS.md— resource tipis, logika bisnis di Service, verifikasiphp -l+php artisan test.
Catatan data: sebagian data contoh di DB dev pernah dihapus sehingga sebaran id/count tidak konsisten. Abaikan anomali data dev — bangun query murni dari skema di atas, uji dengan data buatan sendiri saat QA.
Langkah 1 — Relasi baru di app/Models/CityLeader.php
Tambah 2 relasi (di samping cityCaptain(), province(), city(), district(), posUsers() yang sudah ada):
php
use App\Models\Motorist\User as MotoristUser;
use Illuminate\Database\Eloquent\Relations\HasMany;
/** Semua pos company yang dinaungi city leader ini (conn mysql_pos). */
public function posCompanies(): HasMany
{
return $this->hasMany(PosCompany::class, 'city_leader_id');
}
/** Motorist reseller di bawah city leader ini. */
public function motoristResellers(): HasMany
{
return $this->hasMany(MotoristUser::class, 'city_leader_id')
->where('is_reseller', true);
}
CityLeader::$connection = 'mysql',PosCompany::$connection = 'mysql_pos',Motorist\Userdefaultmysql.whereHas/withCountlintas koneksi tidak aman, jadi hitungan dropshipper dilakukan lewat id list (Langkah 2), bukanwithCount.
Langkah 2 — Service app/Services/CityLeaderSalesService.php
Semua query/agregasi di sini (bukan di UI callback) — sesuai AGENTS.md ("app/Services/*Service.php owns reusable business logic").
php
class CityLeaderSalesService
{
// --- ID list & hitungan -------------------------------------------------
/** @return int[] pos_users.id dropshipper di bawah city leader */
public static function dropshipperIds(int $cityLeaderId): array;
/** @return int[] motorist_users.id yang is_reseller & city_leader_id = X */
public static function motoristIds(int $cityLeaderId): array;
public static function dropshipperCount(int $cityLeaderId): int; // count(dropshipperIds)
public static function motoristCount(int $cityLeaderId): int;
/** @return array<int,int> [cityLeaderId => count] untuk seluruh baris tabel */
public static function countsForAll(): array;
// --- Chart --------------------------------------------------------------
/** @return Collection rows: sale_date, seller_id, seller_name, total_sales */
public static function dropshipperSales(int $cityLeaderId, string $start, string $end): Collection;
/** @return Collection rows: sale_date, seller_id, seller_name, total_sales */
public static function motoristSales(int $cityLeaderId, string $start, string $end): Collection;
// --- Detail modal -------------------------------------------------------
/** @return Collection PosInvoice + items.product utk tanggal & penjual tertentu */
public static function dropshipperInvoices(int $cityLeaderId, string $date, ?int $dropshipperId = null): Collection;
/** @return array{sales: Collection, invoices: array<int, array>, products: array<int, array>} */
public static function motoristInvoiceDetail(int $cityLeaderId, string $date, ?int $motoristId = null): array;
}Spesifikasi query:
A. dropshipperIds()
php
$companyIds = PosCompany::query()->where('city_leader_id', $cityLeaderId)->pluck('id'); // mysql_pos
return PosUser::query()
->whereIn('company_id', $companyIds)
->where('is_dropshipper', true)
->whereNull('deleted_at')
->pluck('id')->all();B. motoristIds()
php
return MotoristUser::query()
->where('city_leader_id', $cityLeaderId)
->where('is_reseller', true)
->pluck('id')->all();C. dropshipperSales() — pola identik PosSalesChartWidget::getData() baris 78-102, diganti filter user:
php
PosInvoice::query()
->selectRaw('DATE(date_time) as sale_date, user_id as seller_id, pos_users.name as seller_name, SUM(pos_invoices.total) as total_sales')
->join('pos_users', 'pos_invoices.user_id', '=', 'pos_users.id')
->whereIn('pos_invoices.user_id', $dropshipperIds)
->whereNull('pos_invoices.deleted_at')
->whereBetween('pos_invoices.date_time', [$start.' 00:00:00', $end.' 23:59:59'])
->whereExists(fn ($q) => $q->select(DB::raw(1))
->from('pos_invoice_items')
->join('pos_products', 'pos_invoice_items.product_id', '=', 'pos_products.id')
->whereColumn('pos_invoice_items.invoice_id', 'pos_invoices.id')
->whereNotNull('pos_products.material_id'))
->groupByRaw('DATE(date_time), pos_invoices.user_id, pos_users.name')
->get();D. motoristSales() — pola PosSalesChartWidget baris 106-126, filter diganti dari mcd.agent_id → mcd.id_motorist:
php
DB::connection('mysql_pos')->table('pos_sales_by_motorist as m')
->selectRaw('DATE(m.created_at) as sale_date, mcd.id_motorist as seller_id, mu.name as seller_name, SUM(mi.total_price) as total_sales')
->join($erpDb.'.motorist_invoices as mi', 'm.motorist_invoice_id', '=', 'mi.id')
->join($erpDb.'.motorist_invoice_items as mii', 'mi.id', '=', 'mii.invoice_id')
->join($erpDb.'.motorist_canvas_details as mcd', 'mii.id_canvas', '=', 'mcd.id')
->join($erpDb.'.motorist_users as mu', 'mcd.id_motorist', '=', 'mu.id')
->whereIn('mcd.id_motorist', $motoristIds)
->whereBetween('m.created_at', [$start.' 00:00:00', $end.' 23:59:59'])
->groupByRaw('DATE(m.created_at), mcd.id_motorist, mu.name')
->get();
$erpDb = (new DatabaseConnection('mysql'))->getNameDB();(lihatPosSalesChartWidget.php:104). Alternatif (bila join item menduplikasiSUM(total_price)): query langsungmotorist_invoices—whereIn('user_id', $motoristIds),SUM(total_price) GROUP BY DATE(created_at), user_id— sama denganMotoristSalesChartWidget.php:70-87. Pilih bila QA di Langkah 9 menunjukkan angka melebih-hitungan.
E. dropshipperInvoices() — pola HasSalesDetailState.php:41-70: PosInvoice::with('items.product'), whereDate('date_time', $date), whereIn('user_id', ...), whereNull('deleted_at'), whereExists material_id, hasil map ke ['id','invoice_number','tax','total'] + invoiceProductsByInvoice[invoiceId] = [...].
F. motoristInvoiceDetail() — pola HasSalesDetailState.php:72-131:
PosSalesByMotorist::whereDate('created_at', $date)→ intersect dengan motorist ids (via joinmotorist_invoice_items/motorist_canvas_detailsdi manaid_motorist IN (...), atau viamotorist_invoices.user_id IN (...)bila memakai alternatif D).Motorist\Invoice::whereIn('id', $ids)→['id','invoice_number','total_price'].Motorist\InvoiceItems::whereIn('invoice_id', $ids)->with('canvas.material')→['product_name'=>canvas.material.nama_barang, 'quantity'=>qty, 'unit_price'=>price, 'subtotal'].
Guard koneksi: if (! (new DatabaseConnection('mysql'))->isKoneksi()) return collect(); (pola PosSalesChartWidget.php:53-59).
Langkah 3 — Widget chart Dropshipper
File: app/Filament/Widgets/CityLeaderDropshipperSalesChartWidget.php
extends Filament\Widgets\ChartWidget,use HasFiltersSchema.protected string $view = 'filament.widgets.city-leader-dropshipper-sales-chart';- Properti publik yang di-pass dari halaman view (Livewire component):php
public ?int $cityLeaderId = null; public array $rawDates = []; getType(): 'bar';filtersSchema(): hanyaDatePicker start_date(defaultnow()->subDays(7)) danend_date(defaultnow()) — tanpa Select agent (scope sudah terkunci ke city leader).getData():- resolve
$cityLeaderId→$dropshipperIds = CityLeaderSalesService::dropshipperIds(...); kosong → return['labels'=>[], 'datasets'=>[]]. $results = CityLeaderSalesService::dropshipperSales(...).- Bangun
$dateRange(loop Carbon,PosSalesChartWidget.php:130-136), set$this->rawDates. $labels = $dateRange->map(fn($d) => Carbon::parse($d)->translatedFormat('j F')).$datasets= perseller_name, warna dari palet 10 warna yang sama,borderRadius => 4.
- resolve
getOptions()—RawJssalinanPosSalesChartWidget::getOptions(); ganti selector dari[data-raw-dates]menjadi[data-dropshipper-raw-dates]agar tidak tabrakan dengan widget lain di halaman yang sama.getHeading()→'Penjualan Dropshipper'.
Langkah 4 — Blade chart Dropshipper
File: resources/views/filament/widgets/city-leader-dropshipper-sales-chart.blade.php
- Salin
resources/views/filament/widgets/pos-sales-chart-widget.blade.php. - Ganti atribut data tanggal:
data-raw-dates→data-dropshipper-raw-dates(dan pembacaan tooltip di langkah 3). - Ganti event dispatch pada
x-on:clickcanvas:blade@this.dispatch('dropshipperBarClicked', { date: rawDates[el.index] || chartInstance.data.labels[el.index], seller: dataset.label, value: dataset.data[el.index], }); - Filter dropdown memakai blok
@if (method_exists($this, 'getFiltersSchema'))yang sudah ada — biarkan.
Langkah 5 — Widget + Blade chart Motorist
app/Filament/Widgets/CityLeaderMotoristSalesChartWidget.php— sama seperti Langkah 3, query memakaiCityLeaderSalesService::motoristSales(), heading'Penjualan Motorist', selector data[data-motorist-raw-dates], eventmotoristBarClicked, propertipublic ?int $cityLeaderId = null;.resources/views/filament/widgets/city-leader-motorist-sales-chart.blade.php— salinmotorist-sales-chart-widget.blade.php(yang sudah memakaidata-motorist-raw-dates), ganti dispatch menjadimotoristBarClickeddengan param{date, seller, value}.
Dua widget terpisah = dua Livewire component.
cityLeaderIddi-pass lewatWidget::make([...])(Langkah 7).
Langkah 6 — Trait detail modal
File: app/Filament/Resources/RingkasanPenjualanCityLeaders/Concerns/HasRingkasanSalesDetailState.php
Pola 1:1 dari app/Filament/Pages/Dashboard/Concerns/HasSalesDetailState.php:
php
trait HasRingkasanSalesDetailState
{
public array $detailInvoices = [];
public array $invoiceProductsByInvoice = [];
public array $motoristSales = [];
public array $motoristProductsByInvoice = [];
public ?string $selectedDate = null;
public ?string $selectedSeller = null;
public ?float $selectedTotal = null;
#[On('dropshipperBarClicked')]
public function onDropshipperBarClicked(string $date, string $seller, float $value): void
{
// set state → CityLeaderSalesService::dropshipperInvoices(...) →
// $this->mountAction('viewDropshipperDetail');
}
#[On('motoristBarClicked')]
public function onMotoristBarClicked(string $date, string $seller, float $value): void
{
// set state → CityLeaderSalesService::motoristInvoiceDetail(...) →
// $this->mountAction('viewMotoristDetail');
}
public function viewDropshipperDetailAction(): Action { ... }
public function viewMotoristDetailAction(): Action { ... }
}Isi action (keduanya, beda sumber state):
Action::make('viewDropshipperDetail')->label('Detail Penjualan')->modalHeading('Detail Penjualan Dropshipper')->modalWidth('4xl')->closeModalByClickingAway(false)->modalSubmitAction(false)schema():Grid::make(2)→Sectioninfo (tanggalviaCarbon::parse($selectedDate)->translatedFormat('j F Y'),total_nominalRp.number_format,total_barang= jumlah qty dari semua produk)->columnSpanFull()+Section 'Detail Invoice'berisiApp\Forms\Components\FormSchema:'state' => 'detailInvoices', kolominvoice_number | tax (numeric) | total (numeric),'expandableState' => 'invoiceProductsByInvoice','expandableTitle' => 'Daftar Produk', kolomproduct_name | quantity | unit_price | subtotal,'canAdd'=>false, 'canEdit'=>false, 'canRemove'=>false, 'popupForm'=>[], 'onAdd'=>fn()=>null, 'actionLabel'=>''(read-only).
- Action motorist: sama, state
motoristSales(invoice_number | total) +motoristProductsByInvoice. - Hitung total dari state array (server-side), jangan dari
valueevent saja.
Langkah 7 — Halaman View
File: app/Filament/Resources/RingkasanPenjualanCityLeaders/Pages/ViewRingkasanPenjualanCityLeader.php
php
class ViewRingkasanPenjualanCityLeader extends Page
{
use HasRingkasanSalesDetailState;
protected static string $resource = RingkasanPenjualanCityLeaderResource::class;
protected CityLeader $record;
public function mount(int|string $record): void
{
$this->record = CityLeader::findOrFail($record);
}
public function getRecord(): CityLeader { return $this->record; }
public function getHeading(): string
{
return 'Ringkasan Penjualan — ' . $this->record->nama;
}
public function getSubheading(): ?string { return $this->record->cityCaptain?->nama; }
public function content(Schema $schema): Schema
{
$id = $this->record->id;
return $schema->components([
Grid::make(2)->schema([
Section::make('Penjualan Dropshipper')
->description('Total penjualan dropshipper per tanggal')
->schema($this->getWidgetsSchemaComponents([
CityLeaderDropshipperSalesChartWidget::make(['cityLeaderId' => $id]),
])),
Section::make('Penjualan Motorist')
->description('Total penjualan motorist reseller per tanggal')
->schema($this->getWidgetsSchemaComponents([
CityLeaderMotoristSalesChartWidget::make(['cityLeaderId' => $id]),
])),
]),
]);
}
}Catatan:
Filament\Resources\Pages\Pagesudah punyacontent()+getWidgetsSchemaComponents()(dariFilament\Pages\Page), jadi tidak perlu blade kustom — view bawaanvendor/filament/filament/resources/views/pages/page.blade.phpmerender$this->content.Widget::make(['cityLeaderId' => $id])→WidgetConfiguration::getProperties()di-pass ke Livewire (lihatFilament\Pages\Page::getWidgetsSchemaComponents()baris 427-434).- Kalau butuh tombol kembali eksplisit, tambahkan
getHeaderActions()denganAction::make('back')->url(RingkasanPenjualanCityLeaderResource::getUrl('index')).
Langkah 8 — Resource + List page + Table
a) app/Filament/Resources/RingkasanPenjualanCityLeaders/RingkasanPenjualanCityLeaderResource.php
php
class RingkasanPenjualanCityLeaderResource extends Resource
{
protected static ?string $model = CityLeader::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedPresentationChartBar;
protected static string|UnitEnum|null $navigationGroup = 'Pos';
protected static ?string $navigationLabel = 'Ringkasan Penjualan City Leader';
protected static ?int $navigationSort = 50; // setelah 4 resource Pos yang ada
protected static ?string $recordTitleAttribute = 'nama';
public static function table(Table $table): Table
{
return RingkasanPenjualanCityLeadersTable::configure($table);
}
// form() TIDAK abstrak — boleh dihilangkan karena read-only.
public static function getPages(): array
{
return [
'index' => ListRingkasanPenjualanCityLeader::route('/'),
'view' => ViewRingkasanPenjualanCityLeader::route('/{record}'),
];
}
// Kunci read-only:
public static function canCreate(): bool { return false; }
public static function canEdit(Model $record): bool { return false; }
public static function canDelete(Model $record): bool { return false; }
public static function canDeleteAny(): bool { return false; }
public static function canRestore(Model $record): bool { return false; }
public static function canForceDelete(Model $record): bool { return false; }
}Nama class/folder tunggal (
RingkasanPenjualanCityLeaderResourcedi folderRingkasanPenjualanCityLeaders/) mengikuti pola repo (CityLeaderResourcediCityLeaders/).
b) Pages/ListRingkasanPenjualanCityLeader.php — extends Filament\Resources\Pages\ListRecords, hanya getHeaderActions() kosong (tanpa CreateAction).
c) Tables/RingkasanPenjualanCityLeadersTable.php
php
return $table
->columns([
TextColumn::make('nama')->label('City Leader')->searchable()->sortable(),
TextColumn::make('cityCaptain.nama')->label('City Captain')->placeholder('-'),
TextColumn::make('jumlah_dropshipper')->label('Jumlah Dropshipper')
->badge()->color('info')
->getStateUsing(fn ($record) => CityLeaderSalesService::dropshipperCount($record->id)),
TextColumn::make('jumlah_motorist')->label('Jumlah Motorist (Reseller)')
->badge()->color('success')
->getStateUsing(fn ($record) => CityLeaderSalesService::motoristCount($record->id)),
])
->filters([
// opsional: SelectFilter city_captain_id / provinsi mengikuti CityLeadersTable
])
->recordUrl(fn ($record) => RingkasanPenjualanCityLeaderResource::getUrl('view', ['record' => $record]))
->recordActions([]) // tanpa EditAction/DeleteAction
->toolbarActions([]); // tanpa DeleteBulkActiongetStateUsingclosure per baris = N query kecil (data city leader sedikit). Bila kelak lambat, ganti kecountsForAll()yang sekali query lalu di-cache — jangan optimasi sebelum perlu.recordUrl(polaMotoristLocationsTable.php:36) membuat seluruh baris klikable.
Langkah 9 — Verifikasi
bash
# 1) syntax semua file baru/diubah
php -l app/Services/CityLeaderSalesService.php
php -l app/Models/CityLeader.php
php -l app/Filament/Widgets/CityLeaderDropshipperSalesChartWidget.php
php -l app/Filament/Widgets/CityLeaderMotoristSalesChartWidget.php
php -l app/Filament/Resources/RingkasanPenjualanCityLeaders/*.php
php -l app/Filament/Resources/RingkasanPenjualanCityLeaders/Pages/*.php
php -l app/Filament/Resources/RingkasanPenjualanCityLeaders/Tables/*.php
php -l app/Filament/Resources/RingkasanPenjualanCityLeaders/Concerns/*.php
# 2) pastikan resource terdaftar & route ada
php artisan route:list --name=ringkasan-penjualan-city-leaders
# 3) test suite
php artisan testDocker: docker compose exec app <command yang sama>.
Checklist QA manual (browser):
- [ ] Nav
Posmenampilkan "Ringkasan Penjualan City Leader" (icon baru, urutan di akhir group). - [ ] Tabel tampil: nama, city captain, 2 kolom jumlah; klik baris → halaman view; tidak ada tombol Create/Edit/Delete/Bulk.
- [ ] View menampilkan 2 section chart + filter tanggal (default 7 hari).
- [ ] Klik bar chart Dropshipper → modal terbuka: tanggal, total nominal, total barang, tabel invoice, expand produk.
- [ ] Klik bar chart Motorist → modal motorist (invoice + produk).
- [ ] Ganti rentang tanggal → chart re-render, modal memakai tanggal yang diklik.
- [ ] Leader tanpa dropshipper/motorist → chart kosong (bukan error), jumlah
0. - [] Siapkan data uji buatan (1 dropshipper + invoice, 1 motorist reseller + invoice) lalu cocokkan totalnya dengan angka modal — bukan dengan data dev lama yang pernah dihapus.
Langkah 10 — Test (disarankan, read-only)
File: tests/Feature/CityLeaderSalesSummaryTest.php
test_table_renders_with_counts— GETRingkasanPenjualanCityLeaderResource::getUrl('index')→ 200, memuat nama leader + angka jumlah.test_view_page_renders— GET urlview→ 200, memuat heading & dua section.test_read_only_resource_has_no_create_route—getUrl('create')melempar exception / route tidak ada.test_service_counts_dropshipper_and_motorist— seedpos_company+pos_users(is_dropshipper)&motorist_users(is_reseller, city_leader_id)→ assert count.test_dropshipper_sales_query_filters_material— seed invoice jasa-only vs barang → hanya barang yang masuk.
(Test butuh koneksi mysql_pos; ikuti pola tests/Feature/CityLeaderAgentAccessTest.php untuk setup koneksi/factory.)
Langkah 11 — Cleanup & commit
- Satu-satunya file lama yang diubah:
app/Models/CityLeader.php(+2 relasi). - Jangan sentuh
HasSalesDetailState/ widget Dashboard yang ada. git addhanya file baru +CityLeader.php.- Commit message mengikuti gaya repo (singkat):
add ringkasan penjualan city leader resource.
3. Risiko & catatan
| Risiko | Mitigasi |
|---|---|
Join pos_sales_by_motorist → motorist_invoice_items fan-out (1 invoice banyak item) membuat SUM(total_price) melebih | Verifikasi saat QA; fallback ke query langsung motorist_invoices.user_id (pola MotoristSalesChartWidget) |
Tabrakan data-raw-dates antar 2 widget di halaman sama | Pakai atribut terpisah: data-dropshipper-raw-dates / data-motorist-raw-dates |
Event dispatch bentrok dengan Dashboard (chartBarClicked) | Nama event khusus: dropshipperBarClicked / motoristBarClicked |
whereHas/withCount lintas koneksi tidak aman | Selalu id-list dulu, whereIn kedua (pola PosUserCityLeaderService) |
| N+1 count di tabel | Batal optimasi dulu; ada countsForAll() bila perlu |
Resource kedua atas CityLeader menimbulkan duplikasi nav | Label & icon berbeda; canCreate/canEdit/canDelete = false |
| Data dev pernah dihapus sehingga angka contoh tidak konsisten | Abaikan; QA dengan data buatan sendiri |
4. Checklist eksekusi singkat
- [ ] 0. ✅ Prasyarat — schema & koneksi DB terverifikasi
- [ ] 1. Relasi
CityLeader::posCompanies()&motoristResellers() - [ ] 2.
CityLeaderSalesService(id list, count, sales, detail) - [ ] 3. Widget chart Dropshipper (+ filter tanggal, event
dropshipperBarClicked) - [ ] 4. Blade chart Dropshipper
- [ ] 5. Widget + blade chart Motorist (event
motoristBarClicked) - [ ] 6. Trait
HasRingkasanSalesDetailState(2 handler + 2 modal action) - [ ] 7. Halaman View (
content()+Widget::make(['cityLeaderId'=>...])) - [ ] 8. Resource + List page + Table (read-only,
recordUrl) - [ ] 9.
php -l+route:list+php artisan test+ QA manual - [ ] 10. Feature test
- [ ] 11. Cleanup & commit