Open40

[laravel][開発編]

ターボターボ

いよいよ開発だーーー!!!!

アプリ名設定

envファイル

APP_NAME=uCRM

サイトのアイコン設定

(私の場合)画像格納場所
C:\xampp\htdocs\uCRM\public\images\site_icon.png

resources\js\Components\ApplicationLogo.vue
<template>
    <div>
        <img src="/images/site_icon.png">
    </div>
</template>

変更後http://127.0.0.1:8000/loginにアクセスし、設定したロゴが表示されていれば完了

ターボターボ

アイテムのテーブルを作成する

🫠ストレージリンクのコマンド

php artisan storage:link

ターボターボ

★Udemy講座ではないメモ★

マイグレーションの作成

https://readouble.com/laravel/8.x/ja/migrations.html

🫠ざっくり

マイグレーション:データベースのバージョン管理

🫠マイグレーションの生成

構文
php artisan make:migration create_flights_table

🫠格納場所

Artisanコマンドmake:migrationを使用して、データマイグレーションを生成する
database/migrationsディレクトリに配置されます。

ターボターボ

Items下準備

🫠モデル作成

php artisan make:model Item -a

-a:all

▼以下を生成

  • モデル
  • ファクトリー
  • マイグレーション
  • シーダー
  • リクエスト
  • リソースコントローラー
  • ポリシー

😎講座で紹介されているページ

https://readouble.com/laravel/9.x/ja/migrations.html

テーブル

database\migrations\2023_12_03_051330_create_items_table.php
    public function up()
    {
        Schema::create('items', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('memo')->nullable();
            $table->integer('price');
            $table->boolean('is_selling')->default(true);
            $table->timestamps();
        });
    }

->nullable():nullを許容する

🫠DB反映&マイグレーションファイル反映

php artisan migrate

実行画面

ターボターボ

モデルルーティング

🫠コード

web.php
use App\Http\Controllers\ItemController;

// 認証していたら表示
Route::resource('items', ItemController::class)->middleware(['auth','verified']);

🫠シェル

php artisan route:list

実行画面

ターボターボ

ユーザーシーダー、ログイン後のロゴ調節

🫠シーディングとは

https://readouble.com/laravel/9.x/ja/seeding.html

🫠先にダミーデータを作成する

database\seeders\UserSeader.php
<?php
<?php

namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;

class UserSeader extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {


        DB::table('users')->insert([
            'name' => 'test',
            'email' => 'test@test.com',
            'password' => Hash::make('password123'),
        ]);
    }
}

public function run()
{
    $this->call([
        UserSeeder::class,
    ]);

}

database\seeders\DatabaseSeeder.php
//抜粋

🫠シェルスクリプト

php artisan migrate:fresh --seed

ターボターボ

シーダー作成の流れ

公式リファレンス
https://readouble.com/laravel/9.x/ja/seeding.html

database/seedersに任意ファイルを追加

例:ItemSeeder.php

②ダミーデータを追記

ItemSeeder.php
<?php

<?php

namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;


class ItemSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        DB::table('items')->insert([
            [
                'name' => 'カット',
                'memo' => 'シャンプー/ブロー込・ロング料金なし・amicoオリジナルの小顔カット',
                'price' => 4700,
            ],
            [
                'name' => 'フルカラー',
                'memo' => 'シャンプー/ブロー込・ロング料金なし・amicoオリジナルのカラー',
                'price' => 7300,
    
            ],
            [
                'name' => 'コスメパーマ',
                'memo' => 'シャンプー/ブロー込・ロング料金なし・amicoオリジナルのケアパーマ',
                'price' => 9500,
            ],

        ]);

    }
}

DatabaseSeeder.phpにクラスを追加

DatabaseSeeder.php
<?php

namespace Database\Seeders;

// use Illuminate\Database\Console\Seeds\WithoutModelEvents;

use App\Models\Item;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        $this->call([
            ItemSeeder::class
        ]);
    }
}

artisanコマンド実行

シーダクラス定義(講座では手動で作成しているためスキップ)
php artisan make:seeder UserSeeder
シーダの実行
php artisan migrate:fresh --seed

ターボターボ

🫠VallidationErrorsコンポーネントがなくなった件の対策

ValidationErrorsファイルを追加する

resources\js\Components\ValidationErrors.vue
<script setup>
import { computed } from 'vue';

const props = defineProps({
    errors: Object
})

const hasErrors = computed(() => Object.keys(props.errors).length > 0);
</script>

<template>
    <div v-if="hasErrors">
        <div class="font-medium text-red-600">問題が発生しました。</div>

        <ul class="mt-3 list-disc list-inside text-sm text-red-600">
            <li v-for="(error, key) in props.errors" :key="key">{{ error }}</li>
        </ul>
    </div>
</template>

import文追加

resources\js\Pages\Inertia\Create.vue
<script setup>
import { reactive } from 'vue';
import { Inertia } from '@inertiajs/inertia'
import BreezeValidationErrors from '@/Components/ValidationErrors.vue'
// 
defineProps({
    errors: Object
})

const form = reactive({
    title: null,
    content: null
})

// フォームを入力したときの処理
const submitFunction = () => {
    Inertia.post('/inertia', form)
}

</script>

以下略
ターボターボ

🫠DBからItemを取得

->get()を忘れないように
SQL文だけでは確定されず取得できない

ItemController.php
class ItemController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
         //DBから取得 
        dd(Item::select('id','name','price','is_selling')->get());
        return Inertia::render('Items/Index');
    }
}

🫠実行結果

コレクション型でシーダーで設定した3件が受け取れていたらOK

🫠リターン追記

ItemController.php
    public function index()
    {
        // 方法1
        // //DBから取得
        // $items = Item::select('id','name','price','is_selling')->get();

        // // 取得したデータを返す
        // return Inertia::render('Items/Index',[
        //     'items' => $items
        // ]);

        //方法2 実行速度を配慮し、行数が少ない方法でかく
        return Inertia::render('Items/Index',[
            'items' => Item::select('id','name','price','is_selling')->get()
        ]);
    }

🫠全コード

コード
ItemController.php
<?php

namespace App\Http\Controllers;

use App\Models\Item;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreItemRequest;
use App\Http\Requests\UpdateItemRequest;
use Inertia\Inertia;

class ItemController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        // 方法1
        // //DBから取得
        // $items = Item::select('id','name','price','is_selling')->get();

        // // 取得したデータを返す
        // return Inertia::render('Items/Index',[
        //     'items' => $items
        // ]);

        //方法2 実行速度を配慮し、行数が少ない方法でかく
        return Inertia::render('Items/Index',[
            'items' => Item::select('id','name','price','is_selling')->get()
        ]);
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
        //
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \App\Http\Requests\StoreItemRequest  $request
     * @return \Illuminate\Http\Response
     */
    public function store(StoreItemRequest $request)
    {
        //
    }

    /**
     * Display the specified resource.
     *
     * @param  \App\Models\Item  $item
     * @return \Illuminate\Http\Response
     */
    public function show(Item $item)
    {
        //
    }

    /**
     * Show the form for editing the specified resource.
     *
     * @param  \App\Models\Item  $item
     * @return \Illuminate\Http\Response
     */
    public function edit(Item $item)
    {
        //
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \App\Http\Requests\UpdateItemRequest  $request
     * @param  \App\Models\Item  $item
     * @return \Illuminate\Http\Response
     */
    public function update(UpdateItemRequest $request, Item $item)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  \App\Models\Item  $item
     * @return \Illuminate\Http\Response
     */
    public function destroy(Item $item)
    {
        //
    }
}

🫠Vueファイル

コントローラーから受け取る場合はdefinePropsを使う

resources\js\Pages\Items\Index.vue
<script setup>
defineProps({
    items: Array
})
</script>
コード全文
resources\js\Pages\Items\Index.vue
<script setup>
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head } from "@inertiajs/vue3";

// コントローラーから受け取る場合は「defineProps」
defineProps({
    items: Array,
});
</script>

<template>
    <Head title="商品一覧" />

    <AuthenticatedLayout>
        <template #header>
            <h2 class="font-semibold text-xl text-gray-800 leading-tight">商品一覧</h2>
        </template>

        <div class="py-12">
            <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
                <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                    <div class="p-6 text-gray-900">
                        <section class="text-gray-600 body-font">
                            <div class="container px-5 py-8 mx-auto">
                                <div class="flex pl-4 mt-4 lg:w-2/3 w-full mx-auto">
                                    <button
                                        class="flex ml-auto text-white bg-green-500 border-0 py-2 px-6 focus:outline-none hover:bg-green-600 rounded"
                                    >
                                        Button
                                    </button>
                                </div>
                                <div class="lg:w-2/3 w-full mx-auto overflow-auto">
                                    <table
                                        class="table-auto w-full text-left whitespace-no-wrap"
                                    >
                                        <thead>
                                            <tr>
                                                <th
                                                    class="px-4 py-3 title-font tracking-wider font-medium text-gray-900 text-sm bg-gray-100 rounded-tl rounded-bl"
                                                >
                                                    ID
                                                </th>
                                                <th
                                                    class="px-4 py-3 title-font tracking-wider font-medium text-gray-900 text-sm bg-gray-100"
                                                >
                                                    商品名
                                                </th>
                                                <th
                                                    class="px-4 py-3 title-font tracking-wider font-medium text-gray-900 text-sm bg-gray-100"
                                                >
                                                    価格
                                                </th>
                                                <th
                                                    class="px-4 py-3 title-font tracking-wider font-medium text-gray-900 text-sm bg-gray-100"
                                                >
                                                    ステータス
                                                </th>
                                            </tr>
                                        </thead>
                                        <tbody>
                                            <tr v-for="item in items" :key="item.id">
                                                <td class="px-4 py-3">{{ item.id }}</td>
                                                <td class="px-4 py-3">{{ item.name }}</td>
                                                <td class="px-4 py-3">
                                                    {{ item.price }}
                                                </td>
                                                <td class="px-4 py-3">
                                                    {{ item.is_selling }}
                                                </td>
                                            </tr>
                                        </tbody>
                                    </table>
                                </div>
                            </div>
                        </section>
                    </div>
                </div>
            </div>
        </div>
    </AuthenticatedLayout>
</template>

ターボターボ

🫠ページ遷移できない問題

Udemy講座名:【Laravel】【Vue.js3】で【CRM(顧客管理システム)】をつくってみよう【Breeze(Inertia)】
環境:Windows11、XAMPP、WSL2利用
https://youtu.be/qr_Dcw35Szs

🫠解決までの道のり

((Udemyの講師に聞いても解決できず、会社の先輩を頼りました😥))

  • node.jsnpmのバージョンをviteに合わせて調整
## 動画でdev用にinstallしているせいで--devオプションが必要でした
composer install --dev

# nvmのインストール(nodeとnpm管理してくれる
# &コマンド1つで簡単にバージョン切り替えが出来る
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash

# パス通す
source ~/.bashrc
source ~/.bash_profile

# node version14 インストール
nvm install 14

# ここ大事!!
npm run build

😎もう一度

遷移しない
npm run dev
遷移する
npm run build

先輩社員から

viteをつかっているのでnpm run devではなく
npm run buildにすることでビルドファイルが生成されます。
なのでnpm run devより安定するかと思われます

ターボターボ

🫠ステータスを見やすくする

v-ifを使うことで条件によって表示を変えられる

変更後
                                            <tr v-for="item in items" :key="item.id">
                                                <td class="border-b-2 border-gray-200 px-4 py-3">{{ item.name }}</td>
                                                <td class="border-b-2 border-gray-200 px-4 py-3">
                                                <td class="border-b-2 border-gray-200 px-4 py-3">{{ item.id }}</td>
                                                    {{ item.price }}
                                                </td>
                                                <td class="border-b-2 border-gray-200 px-4 py-3">
                                                    <span v-if="item.is_selling === 1">販売中</span>
                                                    <span v-if="item.is_selling === 0">停止中</span>
                                                    <!-- {{ item.is_selling }} -->
                                                </td>
                                            </tr>

ターボターボ

🫠jsconfig編集

jsconfig.json
{
    "compilerOptions": {
        "jsx":"preserve",
        "baseUrl": ".",
        "paths": {
            "@/*": ["resources/js/*"]
        }
    },
    "exclude": ["node_modules", "public"]
}

ターボターボ

🌎現時点のPages

│  ComponentTest.vue
│  Dashboard.vue
│  InertiaTest.vue
│  Welcome.vue
│
├─Auth
│      ConfirmPassword.vue
│      ForgotPassword.vue
│      Login.vue
│      Register.vue
│      ResetPassword.vue
│      VerifyEmail.vue
│
├─Inertia
│      Create.vue
│      Index.vue
│      Show.vue
│
├─Items
│      Create.vue
│      Index.vue
│
└─Profile
    │  Edit.vue
    │
    └─Partials
            DeleteUserForm.vue
            UpdatePasswordForm.vue
            UpdateProfileInformationForm.vue

🫠商品登録

商品登録へのリンク追加

Index.vue
<Link as="button" :href="route('items.create')" class="flex ml-auto text-white bg-green-500 border-0 py-2 px-6 focus:outline-none hover:bg-green-600 rounded">商品登録</Link>

商品登録画面作成

Dashboardをコピペし、一部編集する

Create.vue
<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import { Head } from '@inertiajs/vue3';
</script>

<template>
    <Head title="商品一覧" />

    <AuthenticatedLayout>
        <template #header>
            <h2 class="font-semibold text-xl text-gray-800 leading-tight">商品一覧</h2>
        </template>

        <div class="py-12">
            <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
                <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                    <div class="p-6 text-gray-900">You're logged in!</div>
                </div>
            </div>
        </div>
    </AuthenticatedLayout>
</template>

``
ターボターボ

🫠商品登録 フォーム作成

tailblocksのCONTACTを引用

https://tailblocks.cc/

フォーム入力以外のUIを消す
:::
class="w-full" 横幅いっぱいに広げる
class="w-1/2" 横幅の半分の長さに広げる
:::

引用部分
Items/Create.vue
<template>
    <Head title="商品一覧" />

    <AuthenticatedLayout>
        <template #header>
            <h2 class="font-semibold text-xl text-gray-800 leading-tight">商品一覧</h2>
        </template>

        <div class="py-12">
            <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
                <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                    <div class="p-6 text-gray-900">
                        <section class="text-gray-600 body-font relative">
                            <div class="container px-5 py-8 mx-auto">
                                <div class="lg:w-1/2 md:w-2/3 mx-auto">
                                    <div class="flex flex-wrap -m-2">
                                        <div class="p-2 w-1/2">
                                            <div class="relative">
                                                <label
                                                    for="name"
                                                    class="leading-7 text-sm text-gray-600"
                                                    >Name</label
                                                >
                                                <input
                                                    type="text"
                                                    id="name"
                                                    name="name"
                                                    class="w-full bg-gray-100 bg-opacity-50 rounded border border-gray-300 focus:border-green-500 focus:bg-white focus:ring-2 focus:ring-green-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
                                                />
                                            </div>
                                        </div>
                                        <div class="p-2 w-1/2">
                                            <div class="relative">
                                                <label
                                                    for="email"
                                                    class="leading-7 text-sm text-gray-600"
                                                    >Email</label
                                                >
                                                <input
                                                    type="email"
                                                    id="email"
                                                    name="email"
                                                    class="w-full bg-gray-100 bg-opacity-50 rounded border border-gray-300 focus:border-green-500 focus:bg-white focus:ring-2 focus:ring-green-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
                                                />
                                            </div>
                                        </div>
                                        <div class="p-2 w-full">
                                            <div class="relative">
                                                <label
                                                    for="message"
                                                    class="leading-7 text-sm text-gray-600"
                                                    >Message</label
                                                >
                                                <textarea
                                                    id="message"
                                                    name="message"
                                                    class="w-full bg-gray-100 bg-opacity-50 rounded border border-gray-300 focus:border-green-500 focus:bg-white focus:ring-2 focus:ring-green-200 h-32 text-base outline-none text-gray-700 py-1 px-3 resize-none leading-6 transition-colors duration-200 ease-in-out"
                                                ></textarea>
                                            </div>
                                        </div>
                                        <div class="p-2 w-full">
                                            <button
                                                class="flex mx-auto text-white bg-green-500 border-0 py-2 px-8 focus:outline-none hover:bg-green-600 rounded text-lg"
                                            >
                                                Button
                                            </button>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </section>
                    </div>
                </div>
            </div>
        </div>
    </AuthenticatedLayout>
</template>

script部分をInertia/Create.vueから引用

Items/Create.vue
<script setup>
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head } from "@inertiajs/vue3";
import { reactive } from 'vue';
import { Inertia } from '@inertiajs/inertia'

// 
defineProps({
    errors: Object
})

const form = reactive({
    name: null,
    memo: null,
    price: null
})

// フォームを入力したときの処理
const storeItem = () => {
    Inertia.post('/items', form)
}
</script>

🫠商品追加ページに合わせていく

タイトル
Item/Create.vue
<script setup>
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head } from "@inertiajs/vue3";
import { reactive } from 'vue';
import { Inertia } from '@inertiajs/inertia'

// 
defineProps({
    errors: Object
})

const form = reactive({
    name: null,
    memo: null,
    price: null
})

// フォームを入力したときの処理
const storeItem = () => {
    Inertia.post('/items', form)
}
</script>

<template>
    <Head title="商品一覧" />

    <AuthenticatedLayout>
        <template #header>
            <h2 class="font-semibold text-xl text-gray-800 leading-tight">商品一覧</h2>
        </template>

        <div class="py-12">
            <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
                <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                    <div class="p-6 text-gray-900">
                        <section class="text-gray-600 body-font relative">
                        <form @submit.prevent="storeItem">
                            <div class="container px-5 py-8 mx-auto">
                                <div class="lg:w-1/2 md:w-2/3 mx-auto">
                                    <div class="flex flex-wrap -m-2">
                                        <!-- name start -->
                                        <div class="p-2 w-full">
                                            <div class="relative">
                                                <label
                                                    for="name"
                                                    class="leading-7 text-sm text-gray-600"
                                                    >商品名</label
                                                >
                                                <input
                                                    type="text"
                                                    id="name"
                                                    name="name"
                                                    v-model="form.name"
                                                    class="w-full bg-gray-100 bg-opacity-50 rounded border border-gray-300 focus:border-green-500 focus:bg-white focus:ring-2 focus:ring-green-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
                                                />
                                            </div>
                                        </div>
                                        <!-- memo start -->
                                        <div class="p-2 w-full">
                                            <div class="relative">
                                                <label
                                                    for="memo"
                                                    class="leading-7 text-sm text-gray-600"
                                                    >Message</label
                                                >
                                                <textarea
                                                    id="memo"
                                                    name="memo"
                                                    v-model="form.memo"
                                                    class="w-full bg-gray-100 bg-opacity-50 rounded border border-gray-300 focus:border-green-500 focus:bg-white focus:ring-2 focus:ring-green-200 h-32 text-base outline-none text-gray-700 py-1 px-3 resize-none leading-6 transition-colors duration-200 ease-in-out"
                                                ></textarea>
                                            </div>
                                        </div>
                                        <!-- price start -->
                                        <div class="p-2 w-full">
                                            <div class="relative">
                                                <label
                                                    for="price"
                                                    class="leading-7 text-sm text-gray-600"
                                                    >商品名</label
                                                >
                                                <input
                                                    type="number"
                                                    id="price"
                                                    name="price"
                                                    v-model="form.price"
                                                    class="w-full bg-gray-100 bg-opacity-50 rounded border border-gray-300 focus:border-green-500 focus:bg-white focus:ring-2 focus:ring-green-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
                                                />
                                            </div>
                                        </div>

                                        <div class="p-2 w-full">
                                            <button
                                                class="flex mx-auto text-white bg-green-500 border-0 py-2 px-8 focus:outline-none hover:bg-green-600 rounded text-lg"
                                            >
                                                Button
                                            </button>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </form>
                        </section>
                    </div>
                </div>
            </div>
        </div>
    </AuthenticatedLayout>
</template>

関係性をメモ


ターボターボ

🫠フォームのバリデーション

app\Http\Requests\StoreItemRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreItemRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the バリデーション rules that apply to the request.
     *
     * @return array<string, mixed>
     */
    public function rules()
    {
        return [
            'name' => ['request', 'max:50'],
            'memo' => ['request', 'max:255'],
            'price' => ['required', 'numeric'],
        ];
    }
}

🫠コンポーネントをつかう

computed()
Vueの機能
リアタイで検知、計算する

usePage()
Inertiaの機能
マニュアルはShared data
頭にuseがついているのは合成関数

てか合成関数ってなに?調べても理解できなかった))
https://manabitimes.jp/math/2655

ターボターボ

🫠Itemsのフラッシュメッセージ作成中にエラー

$ npm run build

> @ build /mnt/c/xampp/htdocs/uCRM
> vite build

vite v4.5.0 building for production...
✓ 19 modules transformed.
✓ built in 6.13s
[vite:vue] [vue/compiler-sfc] Identifier 'FlashMessage' has already been declared. (5:7)


/mnt/c/xampp/htdocs/uCRM/resources/js/Pages/Items/Index.vue
3  |  import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
4  |  import { Head, Link } from "@inertiajs/vue3";
5  |  import FlashMessage from "@/Components/FlashMessage.vue";
   |         ^
6  |  // コントローラーから受け取る場合は「defineProps」
7  |  defineProps({
file: /mnt/c/xampp/htdocs/uCRM/resources/js/Pages/Items/Index.vue:5:7
error during build:
SyntaxError: [vue/compiler-sfc] Identifier 'FlashMessage' has already been declared. (5:7)

🤔 resources\js\Pages\Items\Index.vueがおかしいらしい

😥 importがおかしいということはresources\js\Components\FlashMessage.vueがおかしいってこと?

🙄 講座見返したらv-if="$page.props.flash.status === 'success'"がおかしかった。

<script setup></script>
<template>
    <!-- フラッシュメッセージなので、リロードしたら消える -->
    <div v-if="$page.props.flash.status === 'success'" class="bg-blue-300 text-white p-4">
        {{ $page.props.flash.message }}
    </div>
</template>

🫠 まだエラーがでる

$ npm run build

> @ build /mnt/c/xampp/htdocs/uCRM
> vite build

vite v4.5.0 building for production...
✓ 18 modules transformed.
✓ built in 6.42s
[vite:vue] [vue/compiler-sfc] Identifier 'FlashMessage' has already been declared. (5:7)

/mnt/c/xampp/htdocs/uCRM/resources/js/Pages/Items/Index.vue
3  |  import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
4  |  import { Head, Link } from "@inertiajs/vue3";
5  |  import FlashMessage from "@/Components/FlashMessage.vue";
   |         ^
6  |
7  |  // コントローラーから受け取る場合は「defineProps」
file: /mnt/c/xampp/htdocs/uCRM/resources/js/Pages/Items/Index.vue:5:7
error during build:
SyntaxError: [vue/compiler-sfc] Identifier 'FlashMessage' has already been declared. (5:7)

翻訳してみた

[vite:vue] [vue/compiler-sfc] 識別子「FlashMessage」はすでに宣言されています。 (5:7)

ビルド中のエラー:
SyntaxError: [vue/compiler-sfc] 識別子「FlashMessage」はすでに宣言されています。 (5:7)

🫠解決

最初から翻訳すればよかった)))
2回importで読み込んでいたのがいけなかった

Index.vue
<script setup>
import FlashMessage from "@/Components/FlashMessage.vue";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head, Link } from "@inertiajs/vue3";
import FlashMessage from "@/Components/FlashMessage.vue"

// コントローラーから受け取る場合は「defineProps」
defineProps({
    items: Array,
});
</script>
Index.vue(解決後)
<script setup>
import FlashMessage from "@/Components/FlashMessage.vue";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head, Link } from "@inertiajs/vue3";

// コントローラーから受け取る場合は「defineProps」
defineProps({
    items: Array,
});
</script
ターボターボ

メモの改行を反映させたい!

🫠JSで改行するための関数をつくる

PHPにあるbl2brメソッドをjsでつくる
※調べたらコピペできそうなものが沢山でてくる

resources\js\common.js
const nl2br = (str) => { 
    var res = str.replace(/\r\n/g, "<br>"); 
    res = res.replace(/(\n|\r)/g, "<br>"); 
    return res; 
}

export { nl2br }

🫠vueファイルでimportする

※追記した部分だけ載せてます

<script setup>
import { nl2br } from `@/common`
</script>
<template>
<div
id="memo" v-html="nl2br(item.memo)"
class="w-full bg-opacity-50 rounded border border-gray-300 focus:border-green-500 focus:bg-white focus:ring-2 focus:ring-green-200 h-32 text-base outline-none text-gray-700 py-1 px-3 resize-none leading-6 transition-colors duration-200 ease-in-out"></div>
</template>

🫠気を付けること

<div></div>内にnl2br{{ item.memo }}を書くと
タグが表示されてしまう。

ターボターボ

🫠商品削除

まず、ルート情報を確認する

php artisan route:list

Show.vueにボタン追加

resources\js\Pages\Items\Show.vue
<div class="mt-20 p-2 w-full">
 <button @click="deleteItem(item.id)" class="flex mx-auto text-white bg-red-500 border-0 py-2 px-8 focus:outline-none hover:bg-red-600 rounded text-lg">削除する</button>
</div>

Show.vueに削除関数追加(script)

resources\js\Pages\Items\Show.vue
// 削除
const deleteItem = id => {
    Inertia.delete(route('items.destroy',{ item: id }),{
        onBefore: () => confirm('本当に削除しますか')
    })
}
全コード
resources\js\Pages\Items\Show.vue
<script setup>
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head,Link } from "@inertiajs/vue3";
import { nl2br } from "@/common";
import { Inertia } from '@inertiajs/inertia';

// コントローラーから受け取る場合は「defineProps」
defineProps({
    item : Object
})

// 削除
const deleteItem = id => {
    Inertia.delete(route('items.destroy',{ item: id }),{
        onBefore: () => confirm('本当に削除しますか')
    })
}
</script>

<template>
    <Head title="商品詳細" />

    <AuthenticatedLayout>
        <template #header>
            <h2 class="font-semibold text-xl text-gray-800 leading-tight">商品詳細</h2>
        </template>

        <div class="py-12">
            <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
                <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                    <div class="p-6 text-gray-900">
                        <section class="text-gray-600 body-font relative">
                       
                            <div class="container px-5 py-8 mx-auto">
                                <div class="lg:w-1/2 md:w-2/3 mx-auto">
                                    <div class="flex flex-wrap -m-2">
                                        <!-- name start -->
                                        <div class="p-2 w-full">
                                            <div class="relative">
                                                <label
                                                    for="name"
                                                    class="leading-7 text-sm text-gray-600"
                                                    >商品名</label
                                                >
                                                <!-- divタグに変更 -->
                                                <div
                                                    id="name"
                                                    class="w-full bg-opacity-50 rounded border border-gray-300 focus:border-green-500 focus:bg-white focus:ring-2 focus:ring-green-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
                                                >{{ item.name }}</div>
                                            </div>
                                        </div>
                                        <!-- memo start -->
                                        <div class="p-2 w-full">
                                            <div class="relative">
                                                <label
                                                    for="memo"
                                                    class="leading-7 text-sm text-gray-600"
                                                    >メモ</label
                                                >
                                                <div
                                                    id="memo" v-html="nl2br(item.memo)"
                                                    class="w-full bg-opacity-50 rounded border border-gray-300 focus:border-green-500 focus:bg-white focus:ring-2 focus:ring-green-200 h-32 text-base outline-none text-gray-700 py-1 px-3 resize-none leading-6 transition-colors duration-200 ease-in-out"
                                                ></div>
                                            </div>
                                        </div>
                                        <!-- price start -->
                                        <div class="p-2 w-full">
                                            <div class="relative">
                                                <label
                                                    for="price"
                                                    class="leading-7 text-sm text-gray-600"
                                                    >商品価格</label
                                                >
                                                <div
                                                    id="price"
                                                    class="w-full bg-opacity-50 rounded border border-gray-300 focus:border-green-500 focus:bg-white focus:ring-2 focus:ring-green-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
                                                >{{ item.price }}</div>
                                            </div>
                                        </div>
                                        <!-- 販売中 -->
                                        <div class="p-2 w-full">
                                            <div class="relative">
                                                <label
                                                    for="status"
                                                    class="leading-7 text-sm text-gray-600"
                                                    >商品価格</label
                                                >
                                                <div
                                                    id="status"
                                                    class="w-full bg-opacity-50 rounded border border-gray-300 focus:border-green-500 focus:bg-white focus:ring-2 focus:ring-green-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
                                                >    <span v-if="item.is_selling === 1">販売中</span>
                                                    <span v-if="item.is_selling === 0">停止中</span>
                                                </div>
                                            </div>
                                        </div>

                                        <div class="p-2 w-full">
                                            <Link as="button" :href="route('items.edit',{ item: item.id })"
                                                class="flex mx-auto text-white bg-green-500 border-0 py-2 px-8 focus:outline-none hover:bg-green-600 rounded text-lg"
                                            >編集する</Link>
                                        </div>
                                        <div class="mt-20 p-2 w-full">
                                            <button @click="deleteItem(item.id)" class="flex mx-auto text-white bg-red-500 border-0 py-2 px-8 focus:outline-none hover:bg-red-600 rounded text-lg"
                                            >削除する</button>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </section>
                    </div>
                </div>
            </div>
        </div>
    </AuthenticatedLayout>
</template>

フラッシュメッセージに追加

resources\js\Components\FlashMessage.vue
    <!-- 削除メッセージ -->
    <div v-if="$page.props.flash.status === 'denger'" class="bg-red-300 text-white p-4">
        {{ $page.props.flash.message }}
    </div>

php編集

app\Http\Controllers\ItemController.php
    public function destroy(Item $item)
    {
        $item->delete();
        return to_route('items.index')
        ->with([
            'message' => '削除しました',
            'status' => 'denger'
        ]);
    }
}

🫠実行結果

ポップアップがでる

フラッシュメッセージがでる

ターボターボ

🫠顧客情報

🔧Customersの下準備

php artisan make:model Customer -a

  • モデル
  • ファクトリー
  • マイグレーション
  • シーダー
  • リクエスト
  • リソースコントローラ
  • ポリシー

💾マイグレーションファイル

database\migrations\2023_12_22_052052_create_customers_table.php
 public function up()
 {
 Schema::create('customers', function (Blueprint $table) {
 $table->id();
 $table->string('name');
 $table->string('kana');
 $table->string('tel')->unique();
 $table->string('email');
 $table->string('postcode');
 $table->string('address');
 $table->date('birthday')->nullable();
 $table->tinyInteger('gender'); // 0男性, 1女性、2その他
 $table->text('memo')->nullable();
 $table->timestamps();
 });
 } 

php artisan migrate実行
customersテーブルが増えているか確認

ダミーデータの日本語対応

config\app.php
    'faker_locale' => 'ja_JP',

ダミーデータ生成

database\factories\CustomerFactory.php
    public function definition()
    {
        return [
            'name' => $this->faker->name,
            'kana' => $this->faker->kanaName,
            'tel' => $this->faker->phoneNumber,
            'email' => $this->faker->email,
            'postcode' => $this->faker->postcode,
            'address' => $this->faker->address,
            'birthday' => $this->faker->dateTime,
            'gender' => $this->faker->numberBetween(0, 2),
            'memo' => $this->faker->realText(50), 
        ];
    }

FakerとFactoryを使うと大量のデータを簡単に作れる
https://qiita.com/tosite0345/items/1d47961947a6770053af

DB Seederにファクトリーを何件作るか。の設定をかく

database\seeders\DatabaseSeeder.php
    public function run()
    {
        $this->call([
            UserSeeder::class,
            ItemSeeder::class
        ]);
        \App\Models\Customer::factory(1000)->create();
        // \App\Models\User::factory(10)->create();

        // \App\Models\User::factory()->create([
        //     'name' => 'Test User',
        //     'email' => 'test@example.com',
        // ]);
    }

php artisan migrate:fresh --seedを実行
ダミーデータが1000件入っていれば良き

🫠ダミーデータの修正

database\factories\CustomerFactory.php
    public function definition()
    {
        $tel = str_replace('-','', $this->faker->phoneNumber);
        $address = mb_substr($this->faker->address, 9);
        return [
            'name' => $this->faker->name,
            'kana' => $this->faker->kanaName,
            'tel' => $tel,
            'email' => $this->faker->email,
            'postcode' => $this->faker->postcode,
            'address' => $address,
            'birthday' => $this->faker->dateTime,
            'gender' => $this->faker->numberBetween(0, 2),
            'memo' => $this->faker->realText(50), 
        ];
    }

ターボターボ

🫠顧客一覧

ルーティング

※追記のみ

routes\web.php
use App\Http\Controllers\CustomerController;
Route::resource('customer',CustomerController::class)->middleware(['auth','verified']);

php artisan route:listを実行 
customerが追加されているか確認

🫠コントローラー

※追記のみ

app\Http\Controllers\CustomerController.php
use Inertia\Inertia;

    public function index()
    {
        return Inertia::render('Customers/Index',[
            'customers' => Customer::select('id','name','kana','tel')->get()
        ]);
    }

🫠vueファイル

コード
resources\js\Pages\Customers\Index.vue
<script setup>
import FlashMessage from "@/Components/FlashMessage.vue";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head, Link } from "@inertiajs/vue3";

// コントローラーから受け取る場合は「defineProps」
defineProps({
    customers: Array,
});
</script>

<template>
    <Head title="顧客一覧" />

    <AuthenticatedLayout>
        <template #header>
            <h2 class="font-semibold text-xl text-gray-800 leading-tight">顧客一覧</h2>
        </template>

        <div class="py-12">
            <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
                <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                    <div class="p-6 text-gray-900">
                        <section class="text-gray-600 body-font">
                            <div class="container px-5 py-8 mx-auto">
                            <FlashMessage />
                                <div class="flex pl-4 mt-4 lg:w-2/3 w-full mx-auto">
                                    <Link as="button" :href="route('customers.create')" class="flex ml-auto text-white bg-green-500 border-0 py-2 px-6 focus:outline-none hover:bg-green-600 rounded">顧客登録</Link>
                                </div>
                                <div class="lg:w-2/3 w-full mx-auto overflow-auto">
                                    <table
                                        class="table-auto w-full text-left whitespace-no-wrap"
                                    >
                                        <thead>
                                            <tr>
                                                <th
                                                    class="px-4 py-3 title-font tracking-wider font-medium text-gray-900 text-sm bg-gray-100 rounded-tl rounded-bl"
                                                >
                                                    ID
                                                </th>
                                                <th
                                                    class="px-4 py-3 title-font tracking-wider font-medium text-gray-900 text-sm bg-gray-100"
                                                >
                                                    氏名
                                                </th>
                                                <th
                                                    class="px-4 py-3 title-font tracking-wider font-medium text-gray-900 text-sm bg-gray-100"
                                                >
                                                    カナ
                                                </th>
                                                <th
                                                    class="px-4 py-3 title-font tracking-wider font-medium text-gray-900 text-sm bg-gray-100"
                                                >
                                                    電話番号
                                                </th>
                                            </tr>
                                        </thead>
                                        <tbody>
                                            <tr v-for="customer in customers" :key="customer.id">
                                                <td class="border-b-2 border-gray-200 px-4 py-3">{{ customer.id }}</td>

                                                <td class="border-b-2 border-gray-200 px-4 py-3">{{ customer.name }}</td>
                                                <td class="border-b-2 border-gray-200 px-4 py-3">{{ customer.kana }}</td>
                                                <td class="border-b-2 border-gray-200 px-4 py-3">{{ customer.tel }}</td>
                                            </tr>
                                        </tbody>
                                    </table>
                                </div>
                            </div>
                        </section>
                    </div>
                </div>
            </div>
        </div>
    </AuthenticatedLayout>
</template>


ナビゲーションメニューに追加

resources\js\Layouts\AuthenticatedLayout.vue
<NavLink :href="route('customers.index')" :active="route().current('customers.index')">顧客管理</NavLink>

~中略~
<ResponsiveNavLink :href="route('customers.index')" :active="route().current('customers.index')">顧客管理</ResponsiveNavLink>


ターボターボ

🫠購入履歴一覧

😎欲しい情報

  • 購買ID
  • 顧客名
  • 合計金額
  • ステータス
  • 購入日

🫶サブクエリ

4つのテーブルをjoin
金額*合計=小計を表示

🐲合計金額を表示

サブクエリで生成したテーブルをもとにgroup byで購入毎の
合計金額を表示

ターボターボ

📂グローバルスコープ用ファイル作成

php artisan make:scope Subtotal

ターボターボ

⌚日時の表示変更

▼現在の日時はこんなかんじ

PHPの問題じゃなくて、console.logで受け取った際にこの形に変換されちゃうらしい😥

コントローラーからデータを受け取ってる
const props = defineProps({
    orders: Object
})
ここでjsの型にかわってる
onMounted(() => {
    console.log(props.orders.data)
})

ライブラリ

便利なライブラリ
https://day.js.org/docs/en/installation/installation

JSで日付のフォーマットを整えるためのライブラリ

#インストール
npm i dayjs@1.11.5 --save

インストールができていたら↓が追加されているはず

package.json
    "dependencies": {
        "dayjs": "^1.11.5",

    }

👽vueファイルにインポート

Index.vue
<script setup>
import dayjs from 'dayjs'
</script>

👽表示部分を編集

フォーマット参考元
https://day.js.org/docs/en/display/format

Index.vue
{{ dayjs(order.created_at).format('YYYY-MM-DD HH:mm:ss')}}

解決!

ターボターボ

🫠データ分析

CRM(顧客管理システムの1つ)
たくさんのデータを分析して資格化する

👻今回は

  • 日別
  • 月別
  • 年別
  • デシル分析
  • RFM分析
    をしていく
ターボターボ

web.php

use App\Http\Controllers\AnalysisController;
Route::get('analysis', [AnalysisController::class, 'index'])->name('analysis'); 

use Inertia\Inertia;を読み込むのを忘れない

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Inertia\Inertia;

class AnalysisController extends Controller
{
    public function index()
    {
        return Inertia::render('Analysis');
    }
}

❔何気に使っている(今回でいうこれ)Inertia::render('Analysis')

Inertia::render関数は第一引数に コンポーネント 、第二引数に プロパティ配列 渡す
https://zenn.dev/misaka/articles/c81fa95f1c00a1

ターボターボ

controllerがインポートされない問題

試したこと
composer dump-autoload
https://note.com/note_fumi/n/n3c0206db55ed
↑変わらなかった

😥実行結果にエラーがでるようになった

怪しそうなコードを触っていたら、やっと実行結果にエラーがでるようになった

Cannot declare class App\Http\Controllers\Api\AnalysisController, because the name is already in use
訳:名前はすでに使用されているため、クラス App\Http\Controllers\Api\AnalysisController を宣言できません

🫠解決

namespace名がかぶっていた

app\Http\Controllers\AnalysisController.php
namespace App\Http\Controllers;

//↓間違い Api直下ファイルとかぶっていた
namespace App\Http\Controllers\Api;
app\Http\Controllers\Api\AnalysisController.php
namespace App\Http\Controllers\Api;
ターボターボ

メモ

select history.id, history.customer_name, sum(history.subtotal) as 
total, history.status, history.created_at 
from (select purchases.id as id, item_purchase.id as pivot_id, 
customers.name as customer_name, 
items.price * item_purchase.quantity as subtotal, 
items.name as item_name, items.price as item_price, 
item_purchase.quantity, purchases.status, purchases.created_at, 
purchases.updated_at 
from purchases 
left join item_purchase on purchases.id = item_purchase.purchase_id 
left join items on item_purchase.item_id = items.id 
left join customers on purchases.customer_id = customers.id 
) as history 
where created_at BETWEEN '2022-08-01' and '2022-08-11'
group by history.id
ターボターボ

vue3-charts

インストールする
npm i vue-chart-3@3.1.8 chart.js@3.9.1

ターボターボ

ファットコントローラーとは

Laravelなどのフレームワークで用いられているコントローラーに様々な処理を任せてしまい、1つのコントローラー内、1つのメソッド内の行数が多くなってしまって肥大化していることを指します。
https://techblog.styleedge.co.jp/entry/2022/02/28/164301

controllerはなるべつコードが少ないほうがいい!
デメリット

  • どこに処理を記述したかが分かりにくく、コードを追いにくい
  • 改修漏れが生じやすい
ターボターボ

😥デシル分析の弱点

  • 検索期間が長期間だと
    過去は優良顧客だったけど現在は通っていないユーザーも含まれてしまう
  • 検索期間が短期間だと
     得られるデータが少ない
     定期的に購入する安定顧客が含まれず、一時的に大きな買い物をしたユーザーが優良と扱われる
ターボターボ

📝RFM分析について

Recency 最新購入日
Frequency 購入回数
Monetary 購入金額合計
3つの軸に分ける事で、
1回のみ高額で購入したユーザー と
定期的に高額ではない商品を購入しているユー
ザーは それぞれ別のグループとして扱われる

🎒開発の流れ

  1. 購買ID毎にまとめる
  2. 会員毎にまとめて最終購入日、回数、合計
    金額を取得
  3. RFMランクを仮設定する
  4. 会員毎のRFMランクを計算する
  5. ランク毎の数を計算する(3を再調整)
  6. RとFで2次元で表示してみる