285 lines
9.8 KiB
PHP
285 lines
9.8 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers\Admin;
|
||
|
||
use App\Http\Controllers\Controller;
|
||
use App\Models\Tax;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Support\Facades\Auth;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||
|
||
class TaxController extends Controller
|
||
{
|
||
/**
|
||
* 一覧:キーワード/適用日範囲で絞り込み + ソート + ページング
|
||
*/
|
||
public function list(Request $request)
|
||
{
|
||
$query = Tax::query();
|
||
|
||
// 絞り込み
|
||
$keyword = trim((string) $request->input('kw'));
|
||
if ($keyword !== '') {
|
||
// 数値型でも互換のため部分一致を残す
|
||
$query->where('tax_percent', 'like', "%{$keyword}%");
|
||
}
|
||
$from = $request->input('from');
|
||
$to = $request->input('to');
|
||
if ($from) {
|
||
$query->whereDate('tax_day', '>=', $from);
|
||
}
|
||
if ($to) {
|
||
$query->whereDate('tax_day', '<=', $to);
|
||
}
|
||
|
||
// ソート(既定:適用日 降順)
|
||
$sort = $request->input('sort', 'tax_day');
|
||
$type = strtolower($request->input('sort_type', 'desc'));
|
||
$allow = ['tax_day', 'tax_percent', 'updated_at', 'created_at', 'tax_id'];
|
||
if (!in_array($sort, $allow, true)) {
|
||
$sort = 'tax_day';
|
||
}
|
||
if (!in_array($type, ['asc', 'desc'], true)) {
|
||
$type = 'desc';
|
||
}
|
||
$query->orderBy($sort, $type);
|
||
|
||
$list = $query->paginate(20)->appends($request->except('page'));
|
||
|
||
return view('admin.tax.list', [
|
||
'taxes' => $list,
|
||
'kw' => $keyword,
|
||
'from' => $from,
|
||
'to' => $to,
|
||
'sort' => $sort,
|
||
'sort_type' => $type,
|
||
]);
|
||
}
|
||
|
||
public function add(Request $request)
|
||
{
|
||
if ($request->isMethod('post')) {
|
||
$data = $request->validate([
|
||
'tax_percent' => ['required', 'numeric', 'min:0', 'max:1000'],
|
||
'tax_day' => ['required', 'date', 'unique:tax,tax_day'],
|
||
]);
|
||
$data['operator_id'] = optional(\Auth::user())->ope_id ?? null;
|
||
$data['tax_percent'] = number_format((float)$data['tax_percent'], 2, '.', '');
|
||
\App\Models\Tax::create($data);
|
||
|
||
return redirect()->route('tax')->with('success', '登録しました');
|
||
}
|
||
|
||
return view('admin.tax.add', [
|
||
'tax' => null,
|
||
'isEdit' => false,
|
||
'isInfo' => false,
|
||
]);
|
||
}
|
||
|
||
public function edit(int $tax_id, Request $request)
|
||
{
|
||
$tax = \App\Models\Tax::findOrFail($tax_id);
|
||
|
||
if ($request->isMethod('post')) {
|
||
$data = $request->validate([
|
||
'tax_percent' => ['required', 'numeric', 'min:0', 'max:1000'],
|
||
'tax_day' => ['required', 'date', 'unique:tax,tax_day,' . $tax->tax_id . ',tax_id'],
|
||
]);
|
||
$data['operator_id'] = optional(\Auth::user())->ope_id ?? null;
|
||
$data['tax_percent'] = number_format((float)$data['tax_percent'], 2, '.', '');
|
||
$tax->update($data);
|
||
|
||
return redirect()->route('tax')->with('success', '更新しました');
|
||
}
|
||
|
||
return view('admin.tax.edit', [
|
||
'tax' => $tax,
|
||
'isEdit' => true,
|
||
'isInfo' => false,
|
||
]);
|
||
}
|
||
|
||
public function info(int $tax_id)
|
||
{
|
||
$tax = \App\Models\Tax::findOrFail($tax_id);
|
||
|
||
return view('admin.tax.info', [
|
||
'tax' => $tax,
|
||
'isEdit' => false,
|
||
'isInfo' => true,
|
||
]);
|
||
}
|
||
|
||
|
||
/**
|
||
* 一括削除(一覧のチェックボックスで送られてくる想定)
|
||
* フォーム側 name="ids[]" の配列を POST
|
||
*/
|
||
public function delete(Request $request)
|
||
{
|
||
$ids = (array) $request->input('ids', []);
|
||
$ids = array_values(array_filter($ids, fn($v) => preg_match('/^\d+$/', (string) $v)));
|
||
|
||
if (empty($ids)) {
|
||
return redirect()->route('tax')->with('error', '削除対象が選択されていません。');
|
||
}
|
||
|
||
Tax::whereIn('tax_id', $ids)->delete();
|
||
|
||
return redirect()->route('tax')->with('success', '削除しました');
|
||
}
|
||
|
||
/**
|
||
* CSVインポート
|
||
* カラム想定: tax_percent, tax_day
|
||
* - 1行目はヘッダ可
|
||
* - tax_day をキーとして「存在すれば更新 / 無ければ作成」
|
||
*/
|
||
public function import(Request $request)
|
||
{
|
||
$request->validate([
|
||
'file' => ['required', 'file', 'mimetypes:text/plain,text/csv,text/tsv', 'max:2048'],
|
||
]);
|
||
|
||
$path = $request->file('file')->getRealPath();
|
||
if (!$path || !is_readable($path)) {
|
||
return redirect()->route('tax')->with('error', 'ファイルを読み込めません。');
|
||
}
|
||
|
||
$created = 0;
|
||
$updated = 0;
|
||
$skipped = 0;
|
||
|
||
DB::beginTransaction();
|
||
try {
|
||
if (($fp = fopen($path, 'r')) !== false) {
|
||
$line = 0;
|
||
while (($row = fgetcsv($fp)) !== false) {
|
||
$line++;
|
||
|
||
// 空行スキップ
|
||
if (count($row) === 1 && trim((string) $row[0]) === '') {
|
||
continue;
|
||
}
|
||
|
||
// ヘッダ行っぽい場合(1行目に 'tax_percent' を含む)
|
||
if ($line === 1) {
|
||
$joined = strtolower(implode(',', $row));
|
||
if (str_contains($joined, 'tax_percent') && str_contains($joined, 'tax_day')) {
|
||
continue; // ヘッダスキップ
|
||
}
|
||
}
|
||
|
||
// 取り出し(列数が足りない場合スキップ)
|
||
$percent = $row[0] ?? null;
|
||
$day = $row[1] ?? null;
|
||
if ($percent === null || $day === null) {
|
||
$skipped++;
|
||
continue;
|
||
}
|
||
|
||
// 正規化 & 検証
|
||
$percent = trim((string) $percent);
|
||
$percent = rtrim($percent, '%');
|
||
$percent = preg_replace('/[^\d.]/', '', $percent) ?? '0';
|
||
$percentF = (float) $percent;
|
||
if ($percentF < 0) {
|
||
$skipped++;
|
||
continue;
|
||
}
|
||
$percentF = (float) number_format($percentF, 2, '.', '');
|
||
|
||
$day = date('Y-m-d', strtotime((string) $day));
|
||
if (!$day) {
|
||
$skipped++;
|
||
continue;
|
||
}
|
||
|
||
// upsert: 適用日ユニーク運用
|
||
$existing = Tax::whereDate('tax_day', $day)->first();
|
||
$payload = [
|
||
'tax_percent' => $percentF,
|
||
'tax_day' => $day,
|
||
'operator_id' => optional(Auth::user())->ope_id ?? null,
|
||
];
|
||
|
||
if ($existing) {
|
||
$existing->update($payload);
|
||
$updated++;
|
||
} else {
|
||
Tax::create($payload);
|
||
$created++;
|
||
}
|
||
}
|
||
fclose($fp);
|
||
}
|
||
|
||
DB::commit();
|
||
return redirect()->route('tax')->with('success', "インポート完了:新規 {$created} 件、更新 {$updated} 件、スキップ {$skipped} 件");
|
||
} catch (\Throwable $e) {
|
||
DB::rollBack();
|
||
return redirect()->route('tax')->with('error', 'インポートに失敗しました:' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* CSVエクスポート:現在の絞り込み/ソート条件を反映
|
||
*/
|
||
public function export(Request $request): StreamedResponse
|
||
{
|
||
$query = Tax::query();
|
||
|
||
$keyword = trim((string) $request->input('kw'));
|
||
if ($keyword !== '') {
|
||
$query->where('tax_percent', 'like', "%{$keyword}%");
|
||
}
|
||
$from = $request->input('from');
|
||
$to = $request->input('to');
|
||
if ($from) {
|
||
$query->whereDate('tax_day', '>=', $from);
|
||
}
|
||
if ($to) {
|
||
$query->whereDate('tax_day', '<=', $to);
|
||
}
|
||
|
||
$sort = $request->input('sort', 'tax_day');
|
||
$type = strtolower($request->input('sort_type', 'desc'));
|
||
$allow = ['tax_day', 'tax_percent', 'updated_at', 'created_at', 'tax_id'];
|
||
if (!in_array($sort, $allow, true)) {
|
||
$sort = 'tax_day';
|
||
}
|
||
if (!in_array($type, ['asc', 'desc'], true)) {
|
||
$type = 'desc';
|
||
}
|
||
$query->orderBy($sort, $type);
|
||
|
||
$filename = 'tax_' . now()->format('Ymd_His') . '.csv';
|
||
|
||
return response()->streamDownload(function () use ($query) {
|
||
$out = fopen('php://output', 'w');
|
||
// Header(設計書の主要カラム)
|
||
fputcsv($out, ['消費税ID', '消費税率', '適用日', '登録日時', '更新日時', '更新オペレータID']);
|
||
$query->chunk(500, function ($rows) use ($out) {
|
||
foreach ($rows as $r) {
|
||
fputcsv($out, [
|
||
$r->tax_id,
|
||
// 画面仕様に合わせたい場合は getDisplayTaxPercentAttribute() に置換可
|
||
is_numeric($r->tax_percent)
|
||
? number_format((float) $r->tax_percent, 2, '.', '')
|
||
: (string) $r->tax_percent,
|
||
optional($r->tax_day)->format('Y-m-d'),
|
||
optional($r->created_at)->format('Y-m-d H:i:s'),
|
||
optional($r->updated_at)->format('Y-m-d H:i:s'),
|
||
$r->operator_id,
|
||
]);
|
||
}
|
||
});
|
||
fclose($out);
|
||
}, $filename, [
|
||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||
]);
|
||
}
|
||
}
|