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', ]); } }