Rapihin setup/member/lot-parking + fitur intercom, exclude WIP controller & file sampah dari git

Kumpulan perubahan yang udah numpuk di working tree: refactor Setup
(Gate/Vehicle/Member), rapihin memberin & stiker, service parking baru
(ParkingUtilsServices, NetworkService), model/migration lot-parking &
DHCP reservation, dan fitur baru Intercom (controller/model/migration/
view).

ReportTransaksiController versi lokal ini WIP (~90%, masih didebug) -
versi yang bener ada di server, jadi dikeluarin dari git (ditambahin
ke .gitignore) biar gak numpuk nindihin punya server pas git pull di
sana. Sekalian keluarin dump mentah tabel softseting (1.8MB, nyasar
ke-commit), file .bak, dan arsip ad-hoc (api.tgz, c.zip) yang emang
bukan source code.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EAJc2mrpj2cSTe4oowLPzQ
This commit is contained in:
2026-10-04 07:15:54 +07:00
co-authored by Claude Sonnet 5
parent efdea9d739
commit 2a9b28581c
91 changed files with 5716 additions and 2470 deletions
+195
View File
@@ -0,0 +1,195 @@
<?php
namespace App\Services\Parking;
use App\Models\ConfigPosNetwork;
use Illuminate\Support\Facades\Log;
use Symfony\Component\Process\Process;
use RuntimeException;
class NetworkService
{
private string $tempFile = "/var/lib/dhcp-panel/reservations.conf";
private string $lastAppliedFile = "/var/lib/dhcp-panel/last-applied";
public function apply(): void
{
Log::info("DHCP APPLY DEBUG", [
"uid" => function_exists("posix_geteuid") ? posix_geteuid() : null,
"user" => get_current_user(),
"temp_file" => $this->tempFile,
"file_exists" => file_exists($this->tempFile),
"dir_writable" => is_writable(dirname($this->tempFile)),
"file_writable" => file_exists($this->tempFile) ? is_writable($this->tempFile) : null,
]);
$config = $this->generate();
if (file_put_contents($this->tempFile, $config) === false) {
throw new RuntimeException("Failed to write temporary DHCP configuration.");
}
$process = new Process(["sudo", "/usr/local/bin/dhcp-reservation", "apply"]);
$process->run();
if (!$process->isSuccessful()) {
throw new RuntimeException(trim($process->getErrorOutput() ?: $process->getOutput()));
}
// if (!$process->isSuccessful()) {
// throw new RuntimeException(...);
// }
// HANYA disimpan kalau apply benar-benar berhasil
$this->saveLastApplied();
}
public function status(): array
{
$service = "isc-dhcp-server";
$running = false;
$process = new \Symfony\Component\Process\Process(["systemctl", "is-active", $service]);
$process->run();
if ($process->isSuccessful()) {
$running = trim($process->getOutput()) === "active";
}
return [
"running" => $running,
"hostname" => $this->hostname(),
"ip" => $this->serverIp(),
"interface" => config("network.dhcp_interface", "eth0"),
"status" => $running ? "running" : "stopped",
"active_clients" => $this->activeClients(),
"last_applied" => $this->lastApplied(),
];
}
public function lastApplied(): ?string
{
if (!file_exists($this->lastAppliedFile)) {
return null;
}
return trim(file_get_contents($this->lastAppliedFile));
}
public function logs(int $limit = 30): array
{
$process = new Process([
"journalctl",
"-u",
$this->dhcpServiceName(),
"-n",
(string) $limit,
"--no-pager",
"-o",
"short",
]);
$process->setTimeout(5);
$process->run();
if (!$process->isSuccessful()) {
return [];
}
return collect(preg_split('/\r\n|\r|\n/', trim($process->getOutput())))->filter()->values()->all();
}
private function dhcpServiceName(): string
{
return config("network.dhcp_service", "isc-dhcp-server");
}
private function hostname(): string
{
return trim(gethostname() ?: php_uname("n"));
}
private function serverIp(): ?string
{
$interface = config("network.dhcp_interface", "enx00e04c6802a9");
$process = new Process(["ip", "-4", "-o", "addr", "show", "dev", $interface]);
$process->run();
if (!$process->isSuccessful()) {
return null;
}
if (preg_match("/inet\s+([0-9.]+)\//", $process->getOutput(), $matches)) {
return $matches[1];
}
return null;
}
private function generate(): string
{
$reservations = ConfigPosNetwork::query()->where("enabled", true)->orderBy("hostname")->get();
$config = "";
foreach ($reservations as $reservation) {
$config .= <<<CONF
host {$reservation->hostname} {
hardware ethernet {$reservation->mac_address};
fixed-address {$reservation->ip_address};
}
CONF;
}
return ltrim($config);
}
private function activeClients(): int
{
$leaseFile = "/var/lib/dhcp/dhcpd.leases";
if (!file_exists($leaseFile)) {
return 0;
}
$content = file_get_contents($leaseFile);
preg_match_all("/lease\s+([0-9.]+)\s*\{(.*?)\}/s", $content, $matches, PREG_SET_ORDER);
$leases = [];
foreach ($matches as $match) {
$ip = $match[1];
$block = $match[2];
preg_match("/binding state\s+(\w+);/", $block, $state);
$leases[$ip] = [
"state" => $state[1] ?? null,
];
}
return collect($leases)->where("state", "active")->count();
}
private function saveLastApplied(): void
{
$directory = dirname($this->lastAppliedFile);
if (!is_dir($directory)) {
mkdir($directory, 0755, true);
}
file_put_contents($this->lastAppliedFile, now()->toDateTimeString());
}
}
@@ -0,0 +1,43 @@
<?php
namespace App\Services\Parking;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class ParkingUtilsServices
{
private function isMirror($user)
{
return $user->statusabsen == 1;
}
private function pgConnectSecond($site_config = null)
{
try {
Config::set("database.connections.pgsql_second", [
"driver" => "pgsql",
"host" => $site_config->site_host,
"user" => $site_config->site_user,
"password" => $site_config->site_password,
"database" => $site_config->site_database,
"port" => $site_config->site_port,
"charset" => "utf8",
"prefix" => "",
"schema" => "public",
]);
return DB::table("pgsql_second");
} catch (\Exception $e) {
Log::error("Failed connect to database " . $e->getMessage());
return null;
}
}
public function useTableParkir($user)
{
$table = $this->isMirror($user) ? "parkir_transaksi" : "transaksi_parkir";
return DB::table("$table as tp");
}
}
+7 -3
View File
@@ -19,16 +19,20 @@ public function parkingTimeCalc($vehicle, $times)
Log::info("Mulai perhitungan tarif dengan durasi : " . $duration . " (Rp. " . $parkingPrice . ")");
// Grace Periode
// if ($duration < $tarifVehicle->waktu_gratis) {
// waktu_gratis kesimpen dalam MENIT (sama kayak interval/waktu2/waktu_tarif
// di bawah), tapi $duration dalam DETIK -- makanya WAJIB dikali 60 dulu biar
// satuannya sama, sama kayak pola interval*60/waktu2*60/waktu_tarif*60 yang
// dipake di bagian lain fungsi ini.
// if ($duration < $tarifVehicle->waktu_gratis * 60) {
// Log::info("GP Rules : ");
// return $parkingPrice;
// } elseif (
// $tarifVehicle->waktu_gratis !== 0 &&
// $tarifVehicle->tarif_periode !== 0
// $tarifVehicle->tarif_period !== 0
// ) {
// Log::info("GP Rules : ");
// $parkingPrice = $parkingPrice + $tarifVehicle->tarif_period;
// $duration = $duration - $tarifVehicle->waktu_gratis;
// $duration = $duration - ($tarifVehicle->waktu_gratis * 60);
// Log::info(
// "Setelah GP : " .
// $parkingPrice .